Balances under 10% of their total were drawn darker and bolder (and the
tray title added "running low"), so a few cards showed the same kind of
number in different shades with nothing saying why. Balances now stay
gray and only turn red at zero, on the card, in the expanded bar and in
the tray. The left percentage is still computed from the total, so bar
length and which lines get picked are unchanged. Per-tier percentages
keep their under-10% emphasis.
The v7 quota redesign moved reset times into the native hover title, which
takes over a second to appear and gives no hint it is there, so in practice
they were gone.
- Provider cards: each quota row now ends with a fixed-width "⏱ 2h30m" slot
(empty when that row has no reset, so rows stay aligned). A merged row
shows its earliest reset; the hover text still lists every tier. The
column only widens (136 → 194px) when some tier has a reset time.
- Expanded quota bars write "Resets in 2h30m" after the value.
- Auth center account rows get the same countdown slot.
- Countdowns are computed at render time from resetsAt instead of being
baked into the line when built, so they no longer go stale between
queries.
- Copilot no longer overwrites the hover detail, which dropped its reset.
Add the v4.0 release notes and the screenshots they reference
(main page, Apps page, Aggregation, Codex model picker, session reader
and usage, in Chinese and English) under assets/release-notes/v4.0.0.
Tu-zi (api.tu-zi.com) is a new-api based aggregator. It is not a
sponsor, so the presets sit in the non-sponsor block of each file.
- Presets for Claude Code, Claude Desktop, Codex, Gemini CLI, Grok
Build, OpenCode, OpenClaw, Hermes and Pi. Endpoints follow the vendor
wiki (wiki.tu-zi.com): Anthropic- and Gemini-style clients use the bare
host; Codex, Grok Build and the OpenCode Anthropic SDK use /v1.
- Claude Code uses ANTHROPIC_API_KEY, as the vendor's own CC Switch
guide tells users to switch from ANTHROPIC_AUTH_TOKEN.
- Claude Desktop uses the bare host although the vendor's Desktop guide
shows /v1: Desktop appends /v1/messages itself, and /v1/v1/messages
returns 404 on this gateway while /v1/messages reaches auth.
- Multi-model apps list Claude Opus 5, Sonnet 5 and Haiku 4.5. The
public pricing catalog marks Opus/Sonnet 5.5 with a "claude code"
endpoint type rather than "anthropic", so they are left out of the
SDK-based clients. Codex defaults to gpt-5.6-sol, Gemini to
gemini-3.8-flash, Grok Build to its default grok-4.5.
- Endpoint candidates add the vendor's Guangzhou (api.ourzhishi.top),
Los Angeles (api.sydney-ai.com) and Cloudflare (apicdn.tu-zi.com)
relays, which front the same backend, in the five apps whose presets
support candidates.
- Display name is localized through providerForm.presets.tuzi:
"兔子API" in zh / zh-TW, "Tu-zi" in en / ja. Search aliases add
兔子 / 兔子api / tuzi.
- 128x128 raster icon from the API site's logo.png, plus metadata.
- Claude preset rows in the selector test go from 72 to 73.
88API (88api.ai), a multi-model API platform run by a Hong Kong company,
joins as a sponsor, placed directly below FluxA in the sponsor order
(preset files and the four README sponsor tables stay in sync; Gemini
and Grok Build have no FluxA preset, so it follows SubRouter there).
- Presets for Claude Code, Claude Desktop, Codex, Gemini CLI, Grok
Build, OpenCode, OpenClaw, Hermes and Pi on api.88api.ai, with
88api.ai as a second endpoint candidate (same new-api deployment, and
the host the vendor's own setup scripts default to). Anthropic- and
Gemini-style clients use the bare host; Codex, Grok Build and the
OpenCode Anthropic SDK use /v1.
- Model lineup is Claude Opus 5.5, Sonnet 5.5, Haiku 4.5 and Fable 5.1,
defaulting to Opus 5.5. Claude Desktop maps the Sonnet/Opus/Haiku role
routes to those models via mappedRoutes. Codex defaults to
gpt-5.6-sol (vendor config script), Gemini to gemini-3.8-flash.
- Adds Claude Sonnet 5.5 to the Pi model catalog (1M context, 128K
output, image input, reasoning; per models.dev) so the Pi preset can
reference it.
- No OpenClaw cost fields: prices vary by the vendor's channel groups.
- Referral link only in apiKeyUrl and README hrefs.
- 128x128 raster icon (square-padded from the vendor logo), metadata
entry, four-locale partnerPromotion copy and a 1200px banner.
- Claude preset rows in the selector test go from 71 to 72.
With more than one saved reset, the reset row opens a dropdown listing
every expiry date (same-day ones grouped, the ones expiring within three
days emphasized):
- Auth Center: the whole reset row is the trigger.
- Provider card: only the "Resets N" segment of the merged second row is
the trigger; the rest of the column still refreshes on click, and the
refresh icon only shows while hovering the refresh area.
Clicking the quota on a provider card already re-queried it, but the
only feedback was a brief dim, so it looked like nothing happened.
- Hovering or focusing the column reveals a small refresh icon left of
the first row (the column is right-aligned, so text never shifts).
- A click spins it until the query settles, for at least 600 ms, then
shows a check on success or a red cross on failure for one second.
Background polling and focus refetches still only dim, so several
cards never spin at once.
- Failure means the column turned into "quota unavailable" or the
refetch itself errored. The second case covers transient failures
inside the keep-last-good window, where the old quota stays on screen
and a check would be a lie; UsageFooter now returns the refetch
promise instead of discarding it. A retry that fails again on an
already failed column also gets the cross.
- After a click the icon stays hidden until the pointer leaves or focus
moves, so it does not pop back right after the check fades. The
button uses aria-disabled instead of disabled while busy, since
Chromium may not deliver mouseleave to a disabled button.
Codex now lets Go/Plus/Pro/Business subscribers bank rate-limit resets
and redeem them later. Query wham/rate-limit-reset-credits alongside
wham/usage (in parallel, best-effort: any failure just leaves the count
out and never fails the quota itself) and count the resets that are
still available and unexpired, the same way CodexBar does, instead of
trusting available_count.
The count shows wherever ChatGPT quota does, since both the Codex CLI
and the managed-account paths go through query_codex_quota:
- Provider card: joins the weekly tier on the second row
("Wk 64% · Resets 1"), reusing the existing merged-row rule.
- Expanded quota and Auth Center: its own row, with the earliest
expiry written where the bar would be.
- Hidden at zero; emphasized when the earliest reset expires within
three days. Resets that expire after the query are dropped at render.
SubscriptionQuota gains an optional resetCredits field (serde default),
so cached snapshots from older builds still deserialize.
The Gemini preset list ended with a "自定义" entry from the original
Gemini integration. The v7 picker already renders its own built-in
custom row, and presetGroup files category "custom" under third-party,
so the add dialog showed two custom options, the extra one without an
icon.
The built-in row starts from GEMINI_DEFAULT_CONFIG, which holds the same
empty base URL and gemini-3.6-flash model, so nothing is lost. The entry
was last in the array, so the index-based `gemini-${index}` ids of the
other presets do not shift.
These presets either had no icon or, for Together AI in Hermes,
referenced a `together` key that did not exist, so the picker fell back
to the first letter without any error.
- TheRouter: the official logo-black.svg inlined with currentColor
strokes, so it follows the theme like other monochrome icons.
- Together AI: the LobeHub together-color SVG, inlined.
- DMXAPI, BaiLing: the 256px frame from each official favicon.ico,
added as URL imports since both are gradients.
- Astron (iFlytek MaaS): the official favicon; only 32px is published.
Each icon gets a metadata entry, and every preset of these providers
across all apps now sets `icon`. All 738 presets now resolve an icon.
The v7 pass gave the name and endpoint fields the required asterisk, but
ApiKeySection never forwarded `required` to ApiKeyInput, so the API Key
field was never marked even though saving warns when it is empty.
ApiKeySection now takes an opt-in `required` and drops it for official,
cloud_provider and disabled fields. Only the forms whose save path checks
the key pass it: Claude Code, Codex (also used by Grok Build), Gemini and
Claude Desktop. Additive apps don't validate the key and stay unmarked.
ApiKeyInput now sets only aria-required, like EndpointField: a native
`required` would let browser validation block the submit before the
"save anyway?" confirmation for an empty key could appear.
v7 sorts presets by display name with no pinning, and no longer reads
primePartner. The sponsor-block comments still claimed file order equals
in-app display order, which would mislead anyone adding a preset.
The v7 visual pass dropped the partner promotion box together with the
promoted ordering. Ordering stays by name, but the promotion text comes
back as the API Key field's hint (caption, secondary text, no box), shown
only when the "Get API Key" link is shown, as before.
ApiKeyInput gains a generic `hint` prop for text below the input.
The model picker's description column repeated the provider name that
the display name already carries, in two languages, on every row:
"经 CC Switch 路由到 X (Routed by CC Switch to X)".
Replace it with the upstream model id and its context window, e.g.
"kimi-k3 · 256K". The display name can be user-chosen and the published
id carries the reserved prefix, so this is the only place the real
upstream id shows up. Claude 1M models show 1M rather than the row's
global window; an unknown window leaves just the model id.
The reserved "ccs-" id prefix is unchanged.
The test replaced a marker in already-serialized JSON with the raw sidecar
path; on Windows its backslashes became invalid escapes, the line failed to
parse and the transcript came back one message short.
With every cached image in use, a newly loaded entry (refs 0) was the only
evictable one and got revoked before its waiting hook acquired it, leaving
the image stuck on the loading placeholder; two loads finishing together
could also evict each other. Count pending waiters per image and skip
images that still have one when evicting.
When several function calls emit empty end events before their delayed
argument deltas, the first delta used to flush every held event, sending
the other calls on without arguments. Argument deltas from any call (held
or not) are now forwarded without releasing anything; held end events are
filled in and released together on the next non-delta event.
Switching Claude Desktop from a model-mapping card back to a direct card
left the local routing service running until some later check. The main
switch, tray click and project apply now record whether the outgoing card
was a mapping card and, if it was and nothing else uses the service, stop
it. Switches that never involved a mapping card leave the service alone,
so a service the user started by hand in settings keeps running.
The heatmap mixed --chart-1 (#0891b2) at 30/50/75/100%, so the darkest
cell was a mid-tone and the whole map read pale (3.68:1 against the card).
Mixing a darker cyan at every level fixed the top but washed out the low
levels.
Move the ramp into --heat-1..4 tokens defined per theme. Light keeps the
lower levels on chart-1 (32%, 62%, solid) and switches only the top level
to #0e7490, same hue one step darker (5.36:1). Dark keeps its previous
ramp, since brighter already means more there. Charts and quota meters
still use --chart-1.
The light --action-bg was #f97315, one unit off the #f97316 the old
add-provider button used via bg-orange-500. Use #f97316 so the accent
matches the pre-redesign orange exactly, recompute the light --ring HSL
from it, and update the design-system doc to the same value.
The divider under the brand row sat about 11px below the page header's
bottom border. Two near-parallel lines that miss each other read as a
misalignment, and the divider only existed in the expanded macOS layout.
Separate the brand from the navigation with spacing instead.
The access token lasts about six hours, not seven days, and the Grok CLI
renews it from the stored refresh_token on its next run. The module doc
now matches the refresh-pending handling from add36d6df.
Grok's access token lasts about six hours and the Grok CLI renews it from
the stored refresh_token on its next run, writing the new token back to
auth.json. When a refresh is permanently rejected, the CLI deletes the
entry. An expired access token that still has a refresh token is now
reported as CredentialStatus::RefreshPending ("token refresh pending")
instead of "sign-in expired", the same as Claude since b68c9d3be. The
`grok login` hint is added only when the sign-in really expired.
The expanded quota view now also shows the "run the CLI command" hint for
refresh pending, for both Claude and Grok.
In light mode the warning text color is nearly the same orange as
clickable text under the default orange accent, and the quota column
itself is a refresh button, so an orange "Fable 5%" read as a link.
Low quota now uses the primary text color at medium weight; used-up
and failed states stay red, and expanded bars keep the amber fill.
With three or more tiers (Claude 5-hour/weekly/Fable, Gemini, some
Coding Plans) the card used to keep the two tiers with the least left,
so the 5-hour line usually vanished and lines jumped around as usage
changed. The first line now always shows the shortest window; the rest
share the second line as short segments (at most two), each colored on
its own. English and Japanese use short tier names to fit the column.
The Stack dialog now tells subscription users to keep the official
account as the default instead of describing what gets dropped.
The duplicate warning under "Set as default" in the more menu is
removed, along with the now unused menu item detail field.
Order is now MCP, Skills, Prompts, Sessions, Authorization, Usage:
the three configs synced to clients stay together, and Usage with its
daily cost sits right above the bottom bar.
The banner arrived ~0.7s after launch and pushed the content area down.
Upgrading users now get a dialog over the page instead, listing what
moved and linking to this version's release notes.
Confirmation is stored as newLayoutNoticeConfirmed in settings.json,
following the existing *_confirmed flags. The welcome dialog sets it
too, so fresh installs never see it.
None of the usage queries had placeholder data, so every new filter,
time range or page cleared the dashboard: hero metrics flashed to an
ellipsis, the trend chart spun, and the request log table collapsed to
its skeleton (scrolling back up and possibly toggling the scrollbar).
Use keepPreviousData on the summary, trend, provider/model stats and
request log queries so the old numbers stay until the new ones land.
The session list is hidden with display:none while the reader is open,
so ResizeObserver reported a 0x0 viewport and the virtualizer unmounted
every row. Going back painted an empty list for a frame until the next
measurement. Ignore 0x0 rects so the existing rows stay in place.
Refocusing the window reloads the prompt list, and reloadPending fed the
buttons' disabled prop directly, so every focus flashed most of the
column grey; toggling a prompt (also on Pi) did the same. Like the MCP
and Skills panels, show the disabled state only after it lasts 300ms
via useDelayedFlag. Handlers still check interactionBlocked, and the
two Pi entry points that relied on disabled alone (toggle, new
template) now check it too. Open forms and dialogs still disable
immediately.
OpenCode, OpenClaw, Hermes and Pi split cards into Added / Available by
the provider ids read back from the live config. Until that query
settles every card counted as not added, so on first entry all cards
rendered under Available and then moved to Added in one jump. Keep the
loading skeleton until the membership query succeeds or fails (only
when there are cards to place; an empty list goes straight to the
empty state).
useProvidersQuery used keepPreviousData, but its key only varies by app,
so the placeholder was always another app's providers: the first switch
to Codex briefly showed Claude's cards, Gemini showed OpenCode's empty
state. Drop the placeholder and instead prefetch every visible app's
list once settings load, keeping the small lists cached so later
switches render the right data on the first frame.
v7 shows an 8px scrollbar that takes layout space, so a page or list
whose content crossed the overflow threshold (filtering the MCP table,
switching usage tabs) shifted every column sideways by 8px. Add a
scroll-stable utility (scrollbar-gutter: stable) and apply it to page
scroll areas, sheet bodies and the large list/table containers; menus,
code blocks and small dialogs keep the default. Drop the unused
scroll-overlay utility.
Cmd+Q, Dock "Quit" and logout/shutdown go through applicationWillTerminate,
which Tauri reports only as RunEvent::Exit, never as ExitRequested. The
exit handler only ran the cleanup on ExitRequested, so quitting this way
left Claude Code / Codex / Gemini / Grok Build pointed at the stopped local
proxy until CC Switch was started again.
Run the same cleanup on RunEvent::Exit: it is spawned onto the async
runtime and the main thread waits for it with an 8 s cap, so a step that
needs the main thread cannot hang the quit. Restart requests also end in
RunEvent::Exit; they are flagged and skipped so the restart keeps Tauri's
default path.
The reader toolbar now ends with the session's total tokens and its cost
at API prices, using the same totals and formatting as the usage
dashboard; hovering shows requests and the input / output / cache split.
The new get_session_usage_summary command sums only rows imported from
session logs: they carry the client's own session ID and are imported
whether or not the request went through the router, while proxy rows
carry an inferred ID and would double count. Sessions with no detail
rows (scanning off, or older than the 30-day rollup) show nothing.
The shared row-to-UsageSummary mapping is extracted so the dashboard
summary and the per-session query use one implementation.
The opaque-state rectifier kept its own three-entry id-prefix table, a
partial copy of the one codex_compaction already uses before sending to
the official upstream. Extract strip_mismatched_item_id there and call it
from both places.
On a rejected retry the rectifier now also drops mismatched ids on
agent_message, tool_search_call and web_search_call items, and non-string
ids, matching the official pass-through scrub. Update the `reasoning`
switch doc to say it strips those ids too.
With a long history, MiniMax's native Responses stream sends
`function_call_arguments.done` and `output_item.done` with empty `arguments`
first, and only then the single delta carrying the full JSON (the arguments
are also in `response.completed`). Codex settles the call on
`output_item.done`, so every tool call failed with "failed to parse function
arguments" and the turn could not do any work.
On the native Responses passthrough to non-official upstreams, hold back a
function call's end events while their arguments are empty, and release them
at the next event that is not an argument delta for that item, filled from the
accumulated deltas or from the matching item in `response.completed`. The
trigger is the protocol violation itself rather than a vendor name, so relays
forwarding MiniMax's stream are covered too. Well-ordered streams pass through
byte for byte; error and non-streaming bodies keep the generic path; requests
are untouched, so prompt caching is unaffected.
Partially revert fefcfafe40. The "All" range and its
53-week heatmap stay; everything else goes back to the earlier dashboard:
- Restore the compact metric cards with the "more metrics" toggle.
- Drop the today / week / month / 7-day average / streak tiles.
- Restore the trend chart's requests default, tab order, single-metric
tooltip and short date format.
- Default the dashboard to today again and bring back the 5s and 10s
refresh options.
- Remove keepPreviousData and the 15s staleTime from the usage queries.
- Color the heatmap with the chart-1 token instead of a fixed green, so it
follows the theme.
- Drop the now-unused locale keys in all four languages, and cover the
chart-to-heatmap swap in the dashboard test.
chatgpt.com's /backend-api/codex/responses sends its SSE without a
Content-Type header. process_response only checked the content type, so
in routing / aggregation mode every official-route turn went down the
non-streaming path:
- the whole body was buffered, so Codex saw no live output until the
turn ended (50-100s)
- parsing the SSE text as JSON failed and the row was logged with zero
usage and is_streaming=0
- the Codex session-log import dedups by token counts, could not match
the zero row, and inserted a second "Codex (Session)" row for the same
turn
Treat a 2xx reply as a stream when the request asked for one and the
upstream sent no Content-Type at all. A declared type still wins: a
gateway that ignores stream:true and labels its JSON text/plain stays
buffered so its usage keeps being parsed. Callers pass the request's
stream flag; the standalone images/search passthrough and the two helper
branches that already decided "not SSE" pass false.
MiniMax's native Responses API signs reasoning items with the thinking text in
`content` (no `encrypted_content`) and gives items ids like `<hex>_msg_N` /
`<hex>_fc_N`. After an aggregation switch from MiniMax to an OpenAI-family
upstream, the request was rejected (`input[N].content` array_above_max_length,
then `invalid_id_prefix`) and the opaque-state rectifier recognised neither, so
the session could not continue.
- Detect the content-array rejection, but only when the request actually
carries a reasoning item with non-empty `content`
- Detect the "Expected an ID that begins with" rejection
- When reasoning items are dropped, also drop message / function_call /
custom_tool_call ids that lack the OpenAI prefix, so both layers clear in
the single retry; `call_id` is untouched
The add/edit provider panels capped the form at 640px and left-aligned it,
leaving a large empty area on the right in ordinary window sizes while the
JSON/TOML editors and model rows were squeezed. Raise the cap to 960px.
Drop the inner max-w-3xl caps on the request headers and extra options
editors; they were inert inside 640px but would now stop short of the
other fields.
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.
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.
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.
Presets: add optional family/versionKey fields so one vendor's regional
and plan variants show as a single row in step 1 (Claude 94 presets ->
71 rows) and as a version switch in step 2. Switching versions after the
user typed into the form asks first, since the form is refilled. The
category field and preset order are unchanged.
Tray: show a dot and a problem tooltip on the icon only while the
routing service failed to start, fell back to direct, or a tray action
failed; spell out that quitting stops the routing service while it is in
use; name the default provider in the Stack submenu title; add the
"restart X to apply" line after tray switches; refresh the tray when the
routing service starts or stops.
Panels: record when the background session scan last finished and show
the newer of that and a manual sync on the usage page; add
resync_mcp_to_apps and resync_skills_to_apps; report per-repo failures
from skill discovery and update checks; report ZIP skills skipped
because the directory is taken. MCP/Skills bulk actions keep applying
to the filtered rows only.
The native menu now lists Claude Code, Claude Desktop, Codex, Gemini CLI
and Grok Build (hidden apps are skipped; additive apps stay out). Each
app row reads "name · mode · provider · quota" and opens a submenu that
states the mode and only offers providers that switching can actually
use: providers that need routing open the app page with the same
needs-routing dialog, official subscriptions are disabled under routing,
and the menu is read-only while failover is on instead of silently
turning failover off. Projects move into the app submenus.
Quota text matches the cards (remaining, "running low" under 10%, no
emoji). A problems section appears only when something needs action:
the routing service is down while apps use it, an app fell back to
direct at startup, or a tray switch failed.
On Windows a left click opens the window and a right click shows the
menu; macOS and Linux keep the menu on left click.
Pages (phase 4):
- Usage: v7 layout (filter row, four metrics with more on demand,
switchable trend, request log / provider / model / pricing tabs, data
sources sheet). Latency becomes output speed: output / (duration -
first token), shown only for requests with at least 100 output tokens;
provider totals sum output and generation time instead of averaging.
- Sessions: single-column list grouped by project or time, drilling into
a reader with resume, find, question jump and copy actions.
- MCP and Skills share one app matrix (neutral checks, retry on failed
writes, column bulk actions with undo); MCP add/edit moves into a
drawer with form/JSON modes; Skills merges installed and discover and
always shows a Pi column.
- Prompts: one page for eight apps including Hermes SOUL.md, enable and
disable as buttons, delete with undo instead of a confirm, copy to
other apps, import existing files; app pages show the active prompt.
- Add provider is two steps: pick a preset (always-on search including
Chinese names and domains, categories inferred for display, sorted by
name with no promoted entries), then fill the form. Edit pages name
the provider and warn when it is the one in use.
- App page menu links to that app's usage.
Visual pass (phase 6):
- Secondary pages cover only the content area so the sidebar stays.
- Cmd/Ctrl+K jumps to apps, pages and settings sections.
- Legacy button variants, primary color, dialogs, toasts and sheets
follow v7 tokens; hard-coded palette classes and 10px text across
the remaining components are replaced with tokens.
Quota on provider cards now reads as remaining ("5-hour 69% left", "Balance 82.10") in the secondary text color, keeping the two tiers with the least left. A tier under 10% turns warning, used up / expired / failed turns danger, and a failed query shows the reason on a second gray line. Reset times and the last update move into the hover text; clicking the quota checks again. Expanded views use one bar style where a shorter bar means less left. Green is gone from quota everywhere.
Switch-style apps (Claude Code, Codex, Gemini CLI, Grok Build) get a mode
row (Direct / Routing / Stack). Tabs only change what is viewed; taking
effect goes through buttons that state the consequence, with a confirm
dialog for entering a proxy mode and a one-step, undoable return to
direct. Provider cards are drawn from a per-mode presentation model
(status, buttons, chips, queue order, disabled reasons) instead of each
card inspecting proxy state, and additive apps get Added / Available
sections.
Backend: entering a mode can pick the route target in the same step,
`get_app_mode` reports the effective mode per app, and startup attach
failures are kept for the UI to surface with a retry.
Settings gains a Local Routing section (service, listen address, apps
using routing, failover tuning) replacing the old proxy tab and toggles.
Claude Desktop gets an access bar for model mapping and service state.
Replace the header toolbar and icon-only app switcher with the v7 shell:
- Sidebar (200px, collapses to a 72px icon rail under 960px, Cmd/Ctrl+\
toggles and remembers the choice): the app list with mode tags and
"needs attention" dots, then Usage (today's cost), Accounts (re-login
dot), MCP, Skills, Prompts and Sessions pinned to the bottom, and an
Apps / Settings footer. Settings swaps the sidebar for its own
directory.
- Every screen gets a 52px page header; Linux in-app window controls
sit at its far right. macOS traffic lights move down to centre in the
44px sidebar top bar.
- Usage, Accounts, MCP, Skills, Prompts (with an app picker) and
Sessions become global pages instead of settings tabs or per-app
toolbar buttons. OpenClaw and Hermes switch to their own pages with a
segmented control; OpenClaw's env, tools and agents merge into one
Config page.
- New Apps page: per-app install / upgrade with path and install
source, batch upgrade, conflict details, manual commands, and the
"show in sidebar" switch that used to live in settings. Tool
management moves out of About into a shared hook; a read-only
list_tool_installations command feeds the page so it does not share
calls with the upgrade preflight.
- Settings regroup into General, App config, Local routing, Network,
Data and About, using row/card layout. Path settings save per
section; switches still save immediately. Skills storage and sync
move to a sheet on the Skills page.
- New opt-in "check app updates on startup" setting (off by default):
when on, a background check puts a neutral dot on Apps.
- Switch adopts the v7 track and action colour; env-conflict banner
joins the page flow instead of overlaying the header.
The old route / Stack / failover toggles stay in the provider page
header until the mode tabs land.
Foundation for the v7 redesign; no layout changes yet.
- Add the v7 tokens (surfaces, text, action, inverse, direct / route / stack,
status, chart, shadows) to index.css and expose them in Tailwind together
with v7 radius, shadow and type-role scales; teach tailwind-merge the new
names so text-body no longer cancels text-fg-1.
- Align the shadcn HSL tokens (background, card, muted, border, ring) with the
v7 palette and use the neutral ring for focus outlines.
- Rename light / dark to Light / Dark Side of the Moon with System first, and
pause transitions while switching so colors don't fade in piece by piece.
- Add v7 button variants (solid, neutral, quiet, direct, route, stack) and
sizes, plus HelpTip / DisabledReason (the "?" card), SegmentedControl,
Notice / NoticeSlot and Sheet.
- Respect the OS reduced-motion setting for framer-motion.
Claude Desktop model-mapping providers send every request through the local
routing service, but the service lifecycle only counted the four routing apps:
when Claude Code left routing, stop_server_if_unused stopped the service Desktop
was still using, and nothing started it on launch.
- stop_server_if_unused now also treats a current Desktop model-mapping
provider as a user of the service.
- ensure_desktop_mapping_service starts the service when that provider is in
use; it runs on startup, after switching a Desktop provider and after
applying a project that covers Claude Desktop.
- The frontend no longer warns that routing must be started when switching to
a Desktop model-mapping provider, since the backend starts it; drop the now
unused isProxyRunning hook argument and proxyReasonClaudeDesktop string.
- Always show the usage button in the header and add an Accounts button; the
usage dashboard has data from session logs even without routing.
- Cmd+, now always opens the General settings tab.
- Remove the dead agents and universal views, which had no entry point but
could still be restored from localStorage.
- Restore thin always-visible scrollbars; keep a .no-scrollbar opt-out.
- Keep the provider card's main action visible; only the secondary icon
buttons still reveal on hover.
- Show the needs-routing badge for every app providerNeedsRouting covers,
which adds the missing Grok Build badge.
- Replace leftover "proxy takeover" wording with Routing mode and refresh the
outdated Accounts description in all four locales.
Opening any Hermes session in the Session Manager failed with "no such
column: created_at": the messages table stores `timestamp` (REAL epoch
seconds). Rework the SQLite reader to follow Hermes' own display
projection:
- Select columns by what the store actually has, so older state.db files
without active/compacted/display_metadata still load.
- Show live rows plus compaction-archived rows (active = 1 OR
compacted = 1); rewound/regenerated rows stay hidden. Collapse the
copies compression re-inserts with Hermes' display identity (role,
content, timestamp, tool_call_id, tool_calls, tool_name), keeping the
first position and the most live copy, and skip model_only rows.
- Order by id: timestamps regress across clock skew and compaction.
- Decode "\0json:" structured content (text + image parts) to text.
- Render assistant tool calls as [Tool: name], matching Codex.
The session list now uses the first user message as summary (and as the
title when Hermes has none) and prefers last_activity_at for recency. The
provider filter dropdown gains a Hermes entry.
Supersedes #6694.
Co-authored-by: h6rime <127593600+h6rime@users.noreply.github.com>