Commit Graph
852 Commits
Author SHA1 Message Date
zhayujieandcowagent 07c1c63100 Merge pull request #3167 from zkjqd/refactor/split-web-channel
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>
2026-09-17 12:07:44 +08:00
zhayujieandcowagent d68644e5e4 fix(console): route the scheduler view at /scheduler, not /tasks
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>
2026-09-17 11:41:46 +08:00
zhayujieandcowagent 370d96bdc8 feat(console): make the sidebar logo go home
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>
2026-09-17 11:41:36 +08:00
zkjqdandcowagent 95dc5d71b3 docs: write the web console README in English
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>
2026-09-17 11:27:56 +08:00
zkjqdandcowagent 7b0ccb62ce Merge branch 'master' into refactor/split-web-channel
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>
2026-09-17 11:27:56 +08:00
zhayujieandcowagent f090a28762 docs(web): update the console docs for path routing
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>
2026-09-17 11:25:31 +08:00
zhayujieandcowagent 1fb6b74bf9 fix(console): keep default tabs out of the route, name the evolution tab
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>
2026-09-17 11:25:21 +08:00
zhayujieandcowagent 2b93812111 chore(web): drop the -old comparison scaffolding and a dead route
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>
2026-09-17 11:25:11 +08:00
zkjqdandcowagent 582d028d53 Merge branch 'master' into refactor/split-web-channel
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>
2026-09-17 10:26:25 +08:00
zkjqdandcowagent c2efb0bbaf feat: route the console on paths, with the console at /
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>
2026-09-16 18:08:38 +08:00
zkjqdandcowagent 5101c0c37c feat: route the console on the address bar
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>
2026-09-16 18:07:25 +08:00
zkjqdandcowagent 98b06f12df fix: stop re-downloading the whole console on every reload
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>
2026-09-16 18:07:25 +08:00
zhayujieandcowagent 723684699a fix: dedupe self-evolution bubble on web console history reload
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>
2026-09-15 18:15:51 +08:00
zkjqdandcowagent 96647538de Merge branch 'master' into refactor/split-web-channel
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>
2026-09-15 16:26:36 +08:00
zhayujie 3bf0429055 Merge branch 'feat/web-console-one-click-update' 2026-09-15 10:34:05 +08:00
zhayujie 43edb84b97 feat: simplify web console updater to a compact sidebar menu 2026-09-15 10:33:55 +08:00
zhayujieandcowagent 1f191022d5 fix: keep create-agent modal footer visible on short screens
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>
2026-09-14 18:24:28 +08:00
tsumon c28fff5f61 feat: add one-click source update from the Web console version menu
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
2026-09-14 14:28:21 +08:00
zhayujieandcowagent 8b3d5f530e feat: clarify multi-agent picker with a main-Agent row and slimmer solo view
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>
2026-09-13 21:22:04 +08:00
zhayujieandcowagent aa4e3fe1e9 fix: render agent-embedded images for non-default agents on web and desktop
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>
2026-09-13 20:56:12 +08:00
zhayujieandcowagent 0771b752c2 fix(web): improve mobile layout of the console
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>
2026-09-13 20:21:36 +08:00
zhayujieandcowagent 6807c99fb0 feat(desktop): sync the model catalog overlay editor to the desktop app
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>
2026-09-13 12:10:10 +08:00
zhayujieandcowagent ddfbc663a9 feat(models): make the provider model catalog an overlay on the presets
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>
2026-09-13 11:46:55 +08:00
alanyz106 6d370cb6e8 feat(web): edit a provider's model catalog from the console
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.
2026-09-12 08:06:54 +00:00
zhayujie bda0505302 Merge branch 'master' of https://github.com/zhayujie/CowAgent 2026-09-12 14:56:36 +08:00
zhayujie d7a005d5ed Merge pull request #3125 from alanyz106/feat/chat-fallback-chain
feat(chat): fallback chain — try backup models in order until one answers
2026-09-12 14:56:12 +08:00
zhayujieandcowagent c407bf6170 fix(qq): auto-reconnect on silently-dead websocket connection
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>
2026-09-12 14:41:47 +08:00
zhayujie 7f9356d0e5 Merge branch 'fix/console-channel-start' 2026-09-12 13:31:35 +08:00
zhayujieandcowagent 61ee3bcbcd fix(web): restore app_module lookup in channel _do_stop
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>
2026-09-12 13:31:12 +08:00
alanyz106 8cb998270b fix(web): resolve live ChannelManager for console channel startup
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.
2026-09-11 08:31:45 +00:00
cowagent 5661685f4e feat(image-generation): support gpt-image-2.5-flare and gpt-image-2.5-sunburst
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.
2026-09-11 15:32:03 +08:00
zhayujieandcowagent a9dfc54c4e fix: tolerate UTF-8 BOM in config.json and stop pre-login 401 polling
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>
2026-09-11 10:41:18 +08:00
zhayujieandcowagent a171038eca feat: support DeepSeek V4.1 Flash (deepseek-flash) as default model
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>
2026-09-10 16:03:01 +08:00
zhayujieandcowagent ead145a7ae feat: add gpt-6-astra model with Responses API tool calling
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>
2026-09-10 12:03:03 +08:00
zhayujieandcowagent a93adb8876 fix: clear engaged model fallback immediately when disabling chat fallback
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>
2026-09-10 11:40:38 +08:00
zkjqdandcowagent 3e1509602a feat: add -old to serve the pre-split console for comparison
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>
2026-09-09 16:09:16 +08:00
zkjqdandcowagent b224934751 fix: load the agent roster before the chat state that reads it
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>
2026-09-09 15:47:05 +08:00
zkjqdandcowagent d43aa0c620 refactor: define navigateTo once instead of wrapping it at load time
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>
2026-09-09 15:22:45 +08:00
zkjqdandcowagent 09049a5d10 refactor: break the chat composer into per-concern scripts
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>
2026-09-09 15:15:35 +08:00
zkjqdandcowagent b02bdbf34b refactor: split console.js into a core/ and views/ script tree
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>
2026-09-09 15:07:04 +08:00
zkjqdandcowagent ce10077be9 refactor: split chat.html into a shell and template fragments
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>
2026-09-09 14:56:12 +08:00
zkjqdandcowagent 70e9b4051d refactor: split console.css into eight per-area stylesheets
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>
2026-09-09 14:41:19 +08:00
alanyz106 571ffad147 fix(chat): offer real models to the fallback picker, keep Save in step
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.
2026-09-09 06:01:37 +00:00
zkjqdandcowagent aae7544991 refactor: assemble console HTML from templates on the server
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>
2026-09-09 12:01:54 +08:00
alanyz106 7f98a3db26 feat(chat): walk an ordered fallback chain instead of one backup model
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.
2026-09-08 15:41:00 +00:00
zhayujieandcowagent d7bb9c68bd fix: show the @-an-Agent input hint only in team conversations
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>
2026-09-08 23:27:15 +08:00
zhayujieandcowagent 8b513aa3b6 feat: add edit icon to skill cards and render skill frontmatter cleanly
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>
2026-09-08 22:05:56 +08:00
zhayujieandcowagent ef52cccf99 fix: clearer channel disconnect confirmation copy
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>
2026-09-08 20:56:54 +08:00
zhayujieandcowagent 483f486c14 fix: make channel disconnect stick and put WeChat channels first
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>
2026-09-08 20:50:41 +08:00
zhayujieandcowagent 9f025cbac6 fix: keep keenable.ai signup guidance in search config copy
Co-authored-by: cowagent <cow@cowagent.ai>
2026-09-08 18:19:14 +08:00