After the "start aggregation with this as default" button went away, the
aggregation preview showed the default-after-switch but had no control to
change it; the default could only be picked in the confirm dialog. Once
in aggregation mode it was an icon-only star next to remove.
Move "set as default" to the top of each card's more menu on the
aggregation tab, in both states, and leave add / remove as the only row
actions:
- In aggregation mode it takes effect at once, also for providers not
added yet (the backend adds the new default to the list).
- In direct mode it only records the choice through set_proxy_route; the
card moves to the default section and the confirm dialog preselects it.
- While routing it is listed but disabled with the reason: the default
and the route target are one pointer, so changing it would switch the
live route from a preview.
On Codex, when the default is an official account and the target is a
third-party provider, the item says the official subscription's models
won't be part of the aggregation. That was previously a silent side
effect of changing the default.
Mode-specific menu items come from the presentation builder
(CardPresentation.menuItems), like the row buttons.
The "used when direct" chip marked the provider the client falls back to
after leaving routing. It only helps on the direct tab, where "back to
direct" acts at once and doesn't name the provider. On the route tab
nothing acts on that provider, and with failover on the chip only showed
up when the provider happened to be outside the queue.
The "last routed" chip in the route preview said which provider "start
routing" would use, but the confirm dialog already preselects and shows
it.
Keep the direct chip on the direct tab only and remove the other one
together with its string.
The route pointer (the aggregation default) could only change while the
app was already in routing or aggregation mode: switch_route_locked
rejects everything else. From direct mode the only way to choose it was
the confirm dialog shown when entering the mode.
Add set_route and the set_proxy_route command. In direct mode it only
records the pointer, to be used the next time the app enters routing or
aggregation; the mode, the direct pointer, the member list and the client
files stay untouched. Once in proxy mode it is a plain route switch.
Unknown providers and official subscriptions that can't be proxied are
rejected either way.
While previewing the route or aggregation tab, every card carried a
"start routing here" / "start aggregation with this as default" button.
Both were shortcuts for the notice's button plus a provider choice, and
the aggregation one sat next to add / remove, which only edit the list.
On Codex it also had a hidden cost: an official account can only be the
default, never a member, so starting from any third-party card silently
dropped the official subscription out of the aggregation.
Remove both card buttons. The notice is the single entry, and the
confirm dialog now has the provider select for aggregation as well as
routing, preselecting the last route and falling back to the direct
provider. When an official account is eligible and another provider is
picked as the aggregation default, the dialog says its models won't be
part of the aggregation. The "also adds" note follows the picked default
instead of the last route.
Preview rows keep only list edits: none on the route tab, add / remove
on the aggregation tab. An official subscription that can't be routed
has no button left to hang the reason on, so it moves to the chip's
tooltip.
Same reasoning as the Chinese rename: from the client's side the mode
behaves like an aggregator provider, so each language borrows the word
its own UI already uses for that provider category.
English uses "Aggregation", matching the "Aggregation" provider category
and the noun form of "Routing": Aggregation mode, aggregated models,
join the aggregation. Japanese uses 集約 (集約モード, 集約したモデル),
replacing the untranslated "Stack" that sat between 直接接続 and
ルーティング.
Covers the locale files, the tray's en/ja labels and their test
assertions, and the English halves of six backend errors. Log messages,
code identifiers and storage keys are unchanged.
From the client's side the Stack mode does what an aggregator provider
does: one endpoint, models from several providers, each request routed
by the model it names. Calling it 聚合 lets users reuse a concept they
already know instead of learning what 叠加 stacks onto.
Labels and tabs read 聚合, sentences 聚合模式 / 聚合的模型, and verb uses
become 加入聚合. zh-TW follows; en/ja keep "Stack". Code identifiers,
storage keys and logs are unchanged. Unrelated uses of 叠加 (the
Volcengine discount copy, two code comments, a CSS comment) are kept.
The last-message summary only covered the state.db scan; transcripts merged
from ~/.hermes/sessions/*.jsonl still showed the first user message. Pick the
last non-empty user/assistant message from the tail and keep the first user
message as the title fallback only, matching the SQLite rows.
The auto-expand only asked whether the first preview lines contained the
query, so an output matching both inside and after the preview kept its tail
folded while the reader counted both hits and the second one was unreachable.
Expand the tail whenever it has a hit of its own.
When the new address or port could not be bound, update_config had already
stopped the old server and written the new config, so the service stayed down
and the next save returned Ok(false) because nothing was running: the UI
reported success while clients still pointed at the old port. On a failed
restart write the previous config back (global four fields or the legacy full
row) and start the server on it again, so a failed save changes nothing.
update_global_proxy_config was routed through ProxyService::update_config, whose
legacy DAO reads the claude row and writes all seven fields into every app row,
so one click on "save and restart" copied Claude's retries and timeouts onto
Codex, Gemini and Grok Build. Add ProxyService::update_global_config that only
writes the four global fields through the global DAO, and share the
restart-or-apply step with the legacy path.
The settings page no longer unmounts after saving, but the saved
app-config-dir baseline was only captured on mount. After saving a new
data directory and choosing "restart later" the save button stayed,
resetting to the old value could not be saved, and every later save
reported "restart required" again. Keep the baseline in state and commit
it to the just-saved value once the override is persisted.
"Diagnose installation conflicts" only stored its result in component
state: no toast when conflicts were found, rows stayed collapsed, and the
"N other installs" entry came from the page-load snapshot that nothing
refreshed, so a conflict that appeared after opening the page had no
visible effect. Write the probe reports back into installReports (both
the manual diagnose and the post-upgrade one), auto-expand rows with
diagnosed conflicts and toast the count.
SheetContent let Radix dismiss on any outside pointer-down, so a stray
click on the dimmed area threw away the MCP / prompt / Pi resource /
route-settings draft without asking. Match DialogContent: ignore outside
clicks by default, and let read-only drawers (request detail, data
sources) opt back in with dismissOnOutsideClick.
The imported entry matches AGENTS.md byte for byte, so Pi treats it as
the enabled prompt and delete_pi_prompt refuses to remove it; the undo
on the import toast could only ever report "cannot delete the enabled
prompt". Show the import toast without an undo.
The drawer's apps state covers every MCP app with hidden ones set to
false, and buildEntry spread it over the existing server's apps, so
overwriting from a batch paste turned the server off in apps hidden from
the sidebar and removed it from their config. Only let the visible
checkboxes override the stored flags.
The v7 tray lists only stack members and queue members in order, but
set_proxy_stack_member, add/remove_from_failover_queue and
update_providers_sort_order never rebuilt it, so the tray kept showing a
provider that had just been removed from the stack, and clicking it
re-added it as the default. Refresh the tray from those commands, the
same as set_auto_failover_enabled already does.
update_global_proxy_config only ran the UPDATE on proxy_config, so with
the service running a new listen address / port took effect at the next
app launch while attached clients kept pointing at the old port, and the
logging switch didn't apply live. Route the listen fields and the logging
flag through ProxyService::update_config (restart on change, runtime
apply otherwise), resync attached routes and rewrite Claude Desktop's
model-mapping gateway profile after a restart. Drop the duplicate toast in
RoutingSection; the mutation already reports success and failure.
Every other provider fills the session summary with the last message and
the list labels it "Last:", but the Hermes scanner put the first user
message there. Query the last displayable user / assistant row per
session for the summary and keep the first user message only as the
title fallback.
Find counted matches inside collapsed tool output and highlighted them,
but the group's open state ignored the query, so the active hit was
invisible. Treat a hit in the output like a hit in a long message: open
the output, and show all lines when the hit is past the preview.
The empty state replaced the whole dashboard, including the tabs, so a
fresh install had no way into model pricing / models.dev sync until some
usage existed. Add a "configure pricing" entry to the empty state that
opens the tabs with pricing selected, without drawing the all-zero
overview.
The failover view offered "add to queue" on Codex official account cards;
the backend rejects them (they route with the client's own login) and the
add mutation had no onError, so nothing happened. Mirror
provider_supports_failover in the UI, dim the card with the reason, and
toast failures from the queue add / remove mutations.
The failover view derived P-numbers, up/down bounds and the "routing"
badge from the index in the provider list it was handed, which is the
search-filtered list. Filtering out P1 relabelled the rest and let the
last visible card move down. Number by the full queue, drop stale ids
against the unfiltered providers in ProviderList, and mark the provider
the backend recorded as the route (falling back to the queue head) rather
than whichever card happens to be first.
The JSON tab shows env / header values as a placeholder and swaps the
original value back in by key when parsing. Renaming a key (or adding
one) while keeping the placeholder found no original and silently stored
"". Report it as a parse error naming the key instead.
After a failed first page the retry handler only reset the page offset,
which was already 0, so the query key never changed and nothing was
requested. Refetch explicitly on the first page; a failed load-more still
restarts from page one.
DropdownMenuContent sat at z-50 while FullScreenPanel is z-60, so the
account menu in the auth center opened from a provider form rendered
behind the panel. Put dropdowns on the same layer as Popover and Select
(z-100).
The v7 card menu lost the guard the old ProviderActions had: the current
provider's delete item was enabled, the backend rejected it and the
confirm dialog stayed open with an unhandled rejection. Mark the direct
provider (and the routed one while routing / stacking) with a delete
reason in every switch-mode view and on Claude Desktop, and close the
confirm dialog even when the delete fails.
OpenClaw doesn't support Skills, so SkillApps::is_enabled_for always
returns false for it, yet sync_to_app still walked ~/.openclaw/skills and
deleted any real directory whose name matched a managed skill. The
provider-switch sync hit this on every switch; the new "resync now"
button made it a one-click action. Skip OpenClaw alongside Claude Desktop
and Pi in both sync_to_app and resync_all_apps.
The add / edit / usage-script panels now only cover the content area, so
the sidebar stays clickable while one is open. Their appId followed
activeApp, so switching apps with the edit panel open saved the Claude
provider into Codex. Close the three panels from selectApp / openPage /
openSettings.
The panel only checked whether data was present, so a failed query fell
through to "Request not found". That is how the ambiguous-column error
in get_request_detail looked like a missing feature.
Show a danger notice with the error message and a retry button when the
query fails, and keep "Request not found" for a lookup that returned
nothing.
get_request_detail joins proxy_request_logs with providers but left
created_at and a dozen other columns without a table alias. Both tables
have created_at, so SQLite rejected the statement with "ambiguous column
name" and every lookup failed, whatever the row.
The bug dates back to the session-log import work but stayed hidden:
RequestDetailPanel existed without being mounted anywhere until the v7
redesign wired row clicks to it. Released versions never ran this query.
Add a regression test covering a routed row, a session-log row and a
missing id; the function had no test before.
Grouping by project now opens with every project collapsed and persists
the projects the user expands (new expandedProjects key; the old
collapsedProjects list had the opposite meaning and is ignored). All
groups open while a search is active so matches are never hidden.
- Add an "All" range. Ranges up to 30 days keep the bar chart; "All"
shows a 53-week daily heatmap. Both default to Tokens and show tokens,
requests and cost (USD) on hover.
- Rebuild the metric cards: large Token and cost cards plus a six-cell
strip, replacing the "more metrics" toggle.
- Show today / this week / this month / 7-day average / streak tiles
under the chart for every range.
- Default the dashboard to the last 24 hours.
- Drop the 5s and 10s auto-refresh options (write events already
invalidate queries; saved values fall back to 30s), keep the previous
data while a new range loads, and treat results as fresh for 15s.
Extract the request-log pager into a shared TablePagination and add
useClientPagination for tables that load all rows at once. Pages hold
20 rows, reset when the range or filters change, and clamp when a
refresh returns fewer rows.
- Run the usage read commands (summary, trends, provider/model stats,
request logs and detail) through spawn_blocking; as sync commands they
ran on the Tauri main thread and froze the window during wide scans.
- Pick the session/proxy dedup strategy per time window: skip it when
either side is empty, and when proxy rows are the minority find the
duplicate session rows once (rowid NOT IN) instead of a correlated
EXISTS per row. Same results as before; a full scan on a session-heavy
database drops from ~0.19s to ~0.05s.
- Cache the request-log total per filter until the connection sees any
write (total_changes), the live end time picks up new rows, or 60s pass.
Unify the visual and interaction rules on top of the v7 redesign.
- Theme color #F97315 for --action-* and --ring; dark mode uses #EA580C
with white text, toned-down mode colors and translucent soft fills
- Buttons mapped to three sizes (28 / 32 / 36); inputs, selects and
search fields share fieldClass; self-drawn checkbox and radio
- HoverTip replaces title on about 100 icon buttons; it stays closed
when a closing menu returns focus to its trigger
- Install tailwindcss-animate so menu, popover, dialog and tooltip
enter/exit animations take effect; sliding segment thumb and tab
underline
- Remove the command palette and the header prompt entry; provider card
primary actions become icon buttons
- Virtualize the session list and the Skills discovery list; optimistic
toggles for MCP and Skills with a 300ms delayed disabled state
- Toasts drawn unstyled with status-colored icons and de-duplicated
- Windows: hide the system title bar and use in-app window controls;
macOS: brand row under the traffic lights
- Focus rings only for keyboard users
- Skills discovery and the prompt list show the last result from a
local cache and refresh in the background
- Drop the trailing ellipsis from 36 dialog-opening labels
- Provider list fills the main area
Co-authored-by: 千羽 <qyinter@users.noreply.github.com>
Session-log requests now show a speed marked with "≈" in the request
log, the request detail and the provider table, computed from the
duration the importers estimate from log timestamps. The estimate
includes the wait for the first token, so it is kept apart from the
exact routed speed: a provider row falls back to it only when it has no
routed data, and the help text says the figure reads low.
Requests under 200 output tokens get no estimate, because the first-token
wait dominates there: measured against long replies, 200-300 token
requests read about a quarter low and 100-200 about 40% low. A higher
bar left most Codex rows empty, since only a quarter of Codex requests
exceed 500 output tokens.
The detail panel shows "Duration (estimated)" for these rows and says
why a short reply has no estimate. Help text is updated in all four
locales.
Requests imported from session logs had no timing, so speed was only
available through the routing service. The importers now derive a
duration from log timestamps and store it in latency_ms (first_token_ms
stays NULL, 0 still means "no timing"). The duration includes the wait
for the first token, so the resulting speed reads lower than the routed
figure; provider stats sum it separately (est_speed_*) and never mix it
into the exact speed. Only rows with at least 200 output tokens and a
duration of at least one second count.
Claude Code: a reply ends at its latest block timestamp. Its start is
found by walking parentUuid up from the first block (apiBlockIndex 0),
skipping attachment rows, to the user message or tool result. The
previous line cannot be used: results of parallel tool calls are written
between blocks of the same reply. The start usually sits before the byte
cursor, so each incremental read looks back 256 KiB for chain and block
info without importing it. A reply seen in two syncs gets its duration
extended when the later blocks arrive.
Codex: rollout layouts differ by version. A token_usage_record marks the
end of the response when present; otherwise token_count does, unless it
was written only after the tool outputs, in which case the last output
item is used. The start is the latest boundary before the first output
item. Output items that old versions wrote after their own token_count
are ignored, and task_started / turn_aborted clear the state so an
interrupted request does not lend its start to the next one.
Rows imported earlier keep latency 0; nothing is backfilled.
Claude Code writes subagent transcripts block by block: every block but
the last carries the output-token count as it stood at that moment
(often single digits) and no stop_reason. When a sync landed between two
blocks, the reply was imported with that interim count, and the final
block was later dropped by the request_id dedup, so its output tokens
and output cost stayed under-counted for good. On one machine 293 of
24,928 subagent replies were affected, about 1.6M output tokens.
A reply that is already stored is now refreshed when a later block
reports more output tokens: tokens and costs are rewritten from the
newer block. If the completed usage matches a row the routing service
logged for the same request, the earlier session-log row was a duplicate
and is removed instead. Only session-log detail rows are touched, and a
refreshed row counts as imported so the UI reloads.
Rows that were already imported short are not backfilled; the cursor has
moved past them.
New installs now start with automatic models.dev pricing sync on, and a
pricing file that omits the flag reads as enabled. Existing installs are
left alone: model-pricing.json has carried an explicit autoSyncEnabled
since v3.20.0, so a stored false stays false.
Provider and model took 54% of the table, which left the five numeric
columns at exactly their content width however wide the window was. Cut
the pair to 40% (18/22, model names run longer) and pin time and app to
their content width so the remainder goes to the numeric columns. At the
default 1000px window there is no spare width and nothing changes.
Drop the app names from the usage filter row and show all seven apps as
icon chips; the name moves to the tooltip and aria-label. With icons the
row always fits, so the width-based "More" overflow menu and its
per-app request counts go away, along with the summary-by-app query that
only fed them. Remove the two unused locale keys.
The dashboard opened on the last 7 days; open on today (local midnight
to now) instead. The trend chart already switches to hourly buckets for
ranges of a day or less.
The tray refreshed a CLI-login official subscription through the usage-script
path, which stored the result in the script cache and emitted a "script"
event. The provider card reads the subscription query instead, so a tray
refresh never reached the card and the two could disagree until the card's
next poll. The tray now has a Subscription source for these providers: it
refreshes through get_subscription_quota and reads only the subscription
cache, the same one the card writes.
xAI OAuth providers keep the script path, and no longer fall back to the
app-wide CLI subscription cache when their own snapshot is missing.
Claude's access token expires every few hours and Claude Code renews it on
its next run. An expired access token with a live refresh token is now
reported as CredentialStatus::RefreshPending ("token refresh pending")
instead of "sign-in expired", on the card, in the accounts center and in the
tray. A missing or expired refresh token, or a 401/403 on a locally valid
token, still reports an expired sign-in.
The v7 pass left a few surfaces on a fixed white background while their
contents still followed the theme:
- Provider card icon tile: monochrome (currentColor) logos turned white on
white and initials fallbacks rendered as dark blobs. Pin the tile's text
to a dark neutral and give the fallback a transparent background; keep
multi-character initials on one line.
- Env conflict banner items: bg-white with theme text hid the variable
name in dark mode. Use the bg-surface token.
- Checkboxes: replace bg-white with bg-surface.
Accounts center: each service is now one card with a help tip, "Add
account" and a ⋯ menu ("Delete all accounts…" from two accounts up).
Account rows show the default and re-login badges, the sign-in date, how
many providers use the account, the remaining quota with a refresh time,
and a ⋯ menu for set default, sign in again and delete. Waiting, loading,
load-failure and login-failure states are drawn in place. Deleting still
confirms first and names the affected providers. Provider forms keep
their account picker.
Hermes memory: the save button moves to the page header and is the only
solid button. The file tabs mark unsaved edits with a dot and keep drafts
when switching. The enable row shows the real file path, or that Hermes
skips the file while it is off; the editor is a plain monospace area; the
meter shows characters remaining and warns when over the limit. When
changes take effect is said in the toasts, and "Adjust limit in Hermes
Web UI" asks before starting the Web UI.
Main now hides per-request speed when the generation window is under
100ms. Apply the same rule to the provider speed totals so the table and
the request log agree, and update the preset row count now that the ETok
presets are gone.
Give each visual signal one meaning across pages.
Modes: only the mode being viewed gets the raised fill; the active mode
keeps its colour and dot. The Stack preview starts Stack from a row
("Start Stack with this as default…"), like the routing preview, so the
dialog no longer picks a default. The mode help moves next to the tabs,
the routing status drops the local address, the two-step explanation and
the "advanced feature" checkbox leave the dialogs, and added cards in
additive apps are white; grey now only marks the default provider.
Navigation: page-level switches use underline tabs (new PageTabs, also
used by Usage), with a smaller second level for OpenClaw config, Hermes
memory and Skills. Segmented controls stay for modes and in-page filters.
Hermes memory keeps unsaved edits when switching files.
Danger: irreversible confirmations use the red button; recoverable ones
(back to direct, remove from app, uninstall Skills, rebuild Codex usage,
delete env vars) no longer use red.
Pages:
- MCP/Skills matrix: one square checkbox shape for every state, and the
column header highlights and shows the app name on hover or focus.
- Skills: drop the update banner; "N updatable" in the header filters
the table and offers "Update all"; the unmanaged banner keeps only
Ignore; the installed count lives on the tab only.
- Add provider: "All" is split by category, names sorted within each;
versions split into plan and region, shown as two segmented rows when
they form a grid and as a dropdown otherwise.
- Usage: the trend chart shows the selected metric only; today's log
rows show the time only and the provider column gets the space.
- Sessions: the reader merges into the page header.
- Accounts: rows say how many providers use them, and removing an
account confirms first and names the affected providers.
- Apps: small neutral switches for "Show"; Settings says "Check for new
versions of your apps at launch".
Also add the provider empty state, a one-time "new layout" notice for
existing users, and a 1040px max width for card lists on wide windows.
Presets: name the Tencent family "Tencent Cloud" (腾讯云), since it also
covers pay-as-you-go plans in some apps, and let a family list its
versions in design order (Kimi by region, Tencent Lite before Pro).
Families without an order keep the preset file order.
Tray: a failed tray switch no longer expires after ten minutes. It stays
until the app switches successfully or its page is opened in the main
window, reported through the new tray_app_page_seen command.
Skills storage and sync: turn the sheet into the spec's dialog with a
real three-way sync method (auto, symlink only, copy only; auto was
shown as symlink before), radio buttons for where the main copy lives,
the real CC Switch skills path with an Open button, and a visible line
explaining why "Move and switch" is unavailable.