The custom onErrorRetry existed only to honor a function-form
retryDelay, which exactly one caller (the app data provider) used for
exponential backoff. Fall back to SWR's built-in error retry and
express that caller's cadence with constant options.
Share no abort signal across retry attempts: after a timeout abort the
old signal stays dead and every later fetch rejects immediately, so
retries only burned backoff time. Also drop the system state query's
staleTime: the wrapper maps it to SWR's dedupingInterval, which
swallowed every other 3s poll tick while it exceeded the interval.
checkListDelay pre-marked every node as checking, help() re-marked it
when picked up and measureDelay marked it a third time on entry, so
each batch run queued three identical pending updates per node. Keep
the pre-mark (immediate feedback for queued nodes) and drop the
redundant marks along the chain.
Remove the never-read _revalidate argument of mutateVerge and its 16
call-site placeholders, seven never-passed props of the split chip
editor, the unconsumed refreshDecorated context member, the now-orphaned
toggleFullscreen control, the unused translate argument of the notice
message handler, the reserved-but-unset ServiceConfig.timeout field and
unreachable branches in parseNoticeExtras.
Doc comment, generated bindings and the prebuild sidecar helpers stop
referring to the legacy Clash Meta name; installer legacy-process
cleanup keeps the old executable names on purpose.
This reverts commit cb751052a4.
Remove the tray latency test and its dashboard URL persistence and cache
changes because the two entry points have diverging behavior.
Preserve the subsequent IPC binding migration and remove generated fields,
input projections, and locale entries that depended on the reverted feature.
The notice handler table was rebuilt on every notice event; build it
once at module scope. withLoading returned a new state even when the
loading set was unchanged, re-rendering store consumers for nothing.
* feat(tray): add a per-group latency test
The tray's proxy group submenus offered no way to test a group: refreshing
the delays shown there meant opening the dashboard and running a test from
the proxy page.
Add a "Test Latency" entry at the head of each group submenu. It measures
the group through the proxy page's own view, so member resolution and
provider attribution match the page exactly: provider nodes are health
checked on their provider, anything else goes through the proxy delay
endpoint, and members the page cannot resolve are skipped. Nested groups
are therefore tested through their currently selected node, as the page
does. The menu is rebuilt when the run finishes.
The test URL follows the page's chain -- the group's own test URL, then the
configured default, then the built-in one -- including the page's validity
check, so a group with a malformed URL falls back where the page falls back.
A group test started from the app rebuilds the tray as well, so the two
views do not drift.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tray): keep latency tests and displayed results consistent
* fix(tray): isolate latency histories and preserve usable menus
* fix(tray): keep settings usable when proxy reads fail
* fix(tray): show selected node latency for proxy groups and display timeouts as T/O
Co-authored-by: wonfen <eiwonfen@gmail.com>
* fix(tray): refresh proxy latency on menu interaction
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: wonfen <eiwonfen@gmail.com>
Add the `pt` frontend and backend locale bundles, register the language in
the language selector, and add a Portuguese README linked from the other
READMEs.
Co-authored-by: Nicolas Pastorello <opastorello@MacBookPro.pastorello.infra>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(contract): export wire contract from backend and generate frontend types
Adds a zero-dependency `frontend-contract` bin that serializes the real
NoticeStatus variants and FrontendEvent payloads, scripts/gen-contract.mjs
which emits the committed src/services/contract.ts, and a CI job that
regenerates it and fails on a non-empty diff. events.ts now derives its
event map from the generated contract instead of hand-typed shapes.
* feat(events): route all backend events through a single event bus and store
Adds src/services/bus (the only listen/emit site: one handler per contract
event plus the frontend-internal test-all event) and the React
context/reducer app store. Migrates the five scattered subscriptions
(layout events, pending failures, proxy refresh, profile update/timer,
test-all), deletes the old glue (events.ts, use-listen, use-layout-events,
LoadingCacheProvider), moves run state into the store, and adds an
architecture gate script wired into CI. Behavior is unchanged; the
run-state query's poll/focus safety net is preserved by the store
provider.
* refactor(notice): make the notice table exhaustive and drop dead wire entries
D5 cleanup, frontend and backend in one commit: removes the six
config_validate::* statuses that never receive a backend send and the
set_config::ok send (enum variants, golden list 41→34, send point in
restart_clash_core), their frontend handlers, and the five i18n keys only
those handlers used. The notice table is now typed as a Record over the
generated NoticeStatus union, so a status without a frontend entry fails
to compile. The info status (sent by the silent updater for breaking
change notices, previously dropped with a console warning) now shows an
info toast — the one deliberate behavior delta.
* feat(mutate): single funnel for user-intent commands and invoke gate
Adds src/services/mutate.ts: one path for optimistic apply, services-layer
invoke, Busy handling, failure rollback, and the error toast, with busy
bookkeeping mirrored into the app store. The patch hooks (verge, clash,
profiles) now route through it. Moves the remaining direct invokes
(unlock ×3, DNS ×4) into cmds.ts and turns on the invoke-only-in-services
architecture gate in CI.
* refactor(settings): route settings-domain commands through the mutate funnel
Ports port saving, webdav/local backup operations, core change/restart/
upgrade, service install/uninstall/repair flows, the system-proxy toggle
queue, the DNS override toggle, and app lifecycle commands onto the single
funnel. Bespoke flows keep their own error handling via errorNotice:false;
simple flows gain the funnel's default error toast and revalidation.
* refactor(profiles): route profile operations through the mutate funnel
Ports profile create/import/reorder/delete/update/patch/save-file and
enhance calls onto the funnel; retry and editor flows keep their own
error handling with errorNotice:false. Per-uid ids keep the update
worker pool concurrent under the funnel's in-flight dedupe.
* refactor(proxies): route proxy/home/misc commands through the mutate funnel
Ports mode switching (page and home card), chain runtime updates, node
selection with tray sync, selection recording, provider updates and
syncs, and lightweight-mode entry onto the funnel; every site keeps its
existing optimistic/revert and error-handling behavior.
* docs: add frontend architecture guardrails for src
Layering rules for the contract flow: generated contract as the only
wire-typing source, event bus as the only listen/emit site, pure-reducer
store for event-derived state, the mutate funnel for user intent, and
invoke only in the services layer — each backed by the CI gates.
* fix(events): close run-state race and restore single error toasts
Self-review findings: the bus now re-reads the run-state snapshot in
onSubscribed so an event racing the store's initial read cannot leave a
stale snapshot (the ordering the old layout listener guaranteed), and the
patch hooks pass errorNotice:false so GuardState/page handlers remain the
only error toast, matching the pre-migration behavior.
* fix(frontend): preserve queued intents and fresh runtime snapshots
* refactor(frontend): trim unused mutation and busy state APIs
* docs(frontend): focus guardrails on architecture ownership
When the service's start type was changed from automatic, startup waited
about two minutes for it before falling back to Sidecar. Detect this
immediately, skip the TUN readiness wait, and offer a reinstall that
restores automatic start.
Refs #8129
When the startup probe finds the service refusing the approved core, the
app falls back to Sidecar with only the generic "could not start the
core" notice. The reason and repair guide appeared only after a
reinstall, which cannot fix a writable volume root (#8045).
Carry the fallback reason to the window and show the same permission
notice as a rejected installation when the core was refused.
Refs #8045
When the service watchdog gave up or the core vanished, the owner
monitor marked the core stopped without telling the user, and on macOS
left the system proxy on the dead mixed port (#8029).
- On SameOwnerFailure, clear the proxy through the helper while the
session is still ours; displaced or unreachable owners still leave the
machine-wide setting alone.
- Retain the stop as a core failure with its own message, recorded
under the lifecycle lock and shown now or on next focus; a running
core resets the notice dedup.
- Log each watchdog restart the service reports, since launchd discards
the service's own log.
When the service refuses the app data root because another uid owns it
(code 1012), show a sticky notice with the chown command for this
account. Repairing the service cannot fix this.
Refs #8026
Show unresolved startup failures when the window becomes visible and clear them when either core mode starts successfully. Cover recovery followed by exit and WebView recreation, use neutral service permission wording, and document the Windows startup fix.
Save the DNS override switch and session confirmation per profile so automatic protection only disables the affected profile. Preserve the legacy switch as the initial preference and show the profile scope beside the settings gear and in the DNS editor.
Refs #7993
Address #7983 with a recovery dialog for Windows service error 1003, including startup refusals received before the UI subscribes. Reinstall the service and require service mode before reporting recovery.
Delay test inputs now reject non-http(s) URLs with a warning toast and
fall back to the default URL instead of erroring every node in the
core. Subscription import and profile card host parsing reuse the same
validator, fixing bare-domain URLs shown as local files and
credential-bearing URLs leaking into the card.
Preserve TUN settings and restore proxy operation after fallback.
Deliver fallback notices after UI subscription and stop retrying
deterministic bundle refusals. Publish fallback state atomically
and avoid conflicting service startup notifications.
Manual update-check failures surfaced as red error toasts that read as
an app fault. Add a warning severity to the notice service and use it
in both check entry points alongside a console.warn; automatic checks
and install failures keep their existing behavior.
Limit GUI authority to saved TUN fields while preserving custom overrides.
Sync provider caches after service startup, reloads and updates.
Publish copies atomically with session, runtime and cache metadata checks.
Removes zero-consumer surface verified by repo-wide grep: the
reference-count render machinery, the never-called clearData chain,
unread snapshot fields (rangeMinutes, reason), dead refresh callbacks
on the connection hooks, the write-only wsRef sync, and the worker's
unreachable DEFAULT_CONFIG placeholder (now definite assignment under
the init-first client contract). No behavior change; the compressed
sampler path, samplerStats diagnostics, and lastTimestamp perf
contract stay. Net -120 lines across six files.
The traffic pipeline re-rendered twice per second (worker snapshot
plus a 1 s now-tick), cloned the 10-minute and 60-minute arrays
through every snapshot, and re-filtered them on the main thread.
Snapshots now carry only the range slice the graph renders, a 1 s
worker-side heartbeat re-broadcasts only when points age out of the
window, and snapshot ingestion pauses while the window is hidden.
Perf harness A/B: hidden-phase webview CPU 2.45% -> 0.77%, main
0.90% -> 0.37%, webview RSS about -3 MB; visible phases unchanged.
Consolidate ordinary query snapshots on SWR and make Profiles own its query key, cache mutation, and invalidation policy.
Impact:
- remove the generic synchronous query Map while keeping dedicated Mihomo WebSocket snapshots
- eliminate duplicate Profiles refreshes while preserving retries and the 800 ms event throttle
- preserve preload-backed Verge updates and system proxy rollback behavior
- scope uptime polling to the Home Clash info card instead of the app lifetime
- leave RunState, Connections, Traffic, backend, and IPC contracts unchanged
- reduce frontend code by 72 net lines without adding dependencies
mihomo's updater truncates its own running executable in place; macOS kills
that under Hardened Runtime and leaves a 0-byte core. Verge now downloads the
release, verifies a staged copy next to the target, and renames it into place,
which never touches the inode the running core executes from.
Closes#6834
* fix(cmd): preserve anyhow context in coded errors
Use alternate error formatting so coded failures retain the full anyhow context chain without changing their wire format.
* fix(macos): skip redundant proxy disable writes
When both proxy modes are off, read macOS state first and skip networksetup if the OS is already clean. Update the guard target only after a successful write.
* feat(proxy): classify system proxy failures
Attach stable, actionable proxy failure codes to anyhow chains while preserving the original diagnostic causes.
* feat(cmd): return structured command failures
Replace encoded error strings with serializable code and detail fields, and update frontend notice consumers to handle the structured shape.
* fix(scripts): use the js-yaml namespace import
Import js-yaml through its ESM namespace so the i18n cleanup script can access load and dump.
* feat(proxy): localize classified proxy failures
Map privilege and direct-fallback codes to localized explanations, and let mapped causes outrank generic operation keys in notices.
* fix(ui): bound notice text and render failure details
Limit resolved notice text without truncating clipboard content, and normalize structured failures embedded in translation parameters.
* feat(proxy): persist unseen proxy failures
Store unresolved classified failures by code, notify the frontend when the table changes, and show each failure sequence once when a visible window can receive it.
* feat(proxy): report tray and hotkey toggle failures
Record classified failures from tray and hotkey proxy toggles so they appear when a visible window is available.
* fix(ui): restore the proxy switch after failed toggles
Refresh frontend proxy state after a failed toggle so the switch reflects the rolled-back configuration.
* feat(proxy): stop the guard after repeated failures
Report guard outcomes, stop after the failure threshold, and persist the classified failure for the frontend.
* fix(macos): preserve proxy during service handoff
Stop a sidecar being handed to the service without clearing proxy state that the new owner must inherit.
* feat(macos): classify misplaced sidecar state
Distinguish a refused sidecar proxy write from a machine that still needs the privileged service installed.
* feat(ui): recover misplaced sidecar notices
Offer a restart action for macOS proxy failures caused by a ready service while the core still runs as a sidecar.
* feat(ui): install the service from proxy notices
Let privilege notices install the macOS service, restart the core, and report the runtime mode that was actually reached.
* feat(cmd): attach requested proxy operations
Carry the requested proxy direction with structured command failures when the originating command knows it.
* feat(sysproxy): drain guards before OS access
Wait for the local proxy guard to stop before OS reads, service handoff, or final writes, and refuse transitions when it does not drain.
* feat(sysproxy): recover partial proxy writes
Use write progress to choose whether to restore the old guard or compensate with a disabled proxy state, while preserving actionable errors.
* feat(sysproxy): skip already-applied proxy writes
Compare macOS proxy snapshots with global, PAC, and disabled targets so matching state avoids redundant privileged writes.
* feat(proxy): report background proxy failures
Persist and surface classified proxy failures from lifecycle paths that have no direct caller awaiting the result.
* feat(ui): move proxy recovery into a dialog
Guide users through installing or switching to the service after a refused proxy write, and suppress duplicate toast reports.
* fix(ui): close proxy dialog after service recovery
Dismiss the recovery dialog once the core reaches service mode, even when the original proxy request remains pending.
* feat(ui): retry proxy request after service recovery
Replay the requested proxy direction after the core reaches service mode, while leaving incidental restores unchanged.
* fix(proxy): refresh state after successful apply
Publish the proxy refresh from the shared apply path after releasing the lifecycle lock, and remove the tray-specific duplicate.
* feat(ui): show proxy recovery progress
Display installation, restart, and proxy-apply stages while keeping the primary action spinner visible during recovery.
* feat(ui): route unavailable TUN through service recovery
Let the TUN switch request service recovery and restore its requested state after the core restarts in service mode.
* fix(ui): avoid duplicate TUN recovery errors
Roll back deferred TUN toggles without reporting an error, and publish configuration refreshes from the transaction commit path.
* chore: update changelog & pinlock
* fix(core): resolve platform-specific clippy warnings
* fix(core): detect Windows wildcard TCP conflicts
Windows permits a specific exclusive bind beside an existing non-exclusive wildcard listener. Consult the system TCP listener table so the probe reports that overlap as a conflict.
* fix(core): put the user's node choices back when the core starts
mihomo restores the node picked in each group from the `cache.db` it keeps
in the directory it was started against. The app relied on that entirely,
and it does not hold: `profile.store-selected` comes from a merge template
a user is free to replace, and a service older than the durable runtime
generation starts every core in a directory nothing has ever run in. Both
end the same way — every `select` group falls back to the first entry in
its `proxies:` list.
The app already knows better. It records each selection in the profile,
and `activate_selected_nodes` already reconciles that record against the
running core — it was only ever called when switching profiles, never when
a core came up. Call it from the three places a core process becomes
ready. Repeat calls supersede each other, so no coordination is needed
between the start paths.
That makes the profile authoritative, which turns any selection it does
not know about into one the next start silently undoes. The chain proxy
moved a group without recording it, so recording is now one shared hook
that both it and the proxy list go through.
Restoration is spawned, not awaited: it polls the core until its groups
are readable, and a start must not wait for that.
* fix(tray): record the node the tray switched to
Restoring the profile's selections when a core starts made the profile
authoritative, which turns any selection it never learned about into one
the next start silently undoes. The proxy list and the chain proxy both
record; the tray did not.
So switching node from the tray survived only until the next restart, and
then reverted to whatever the profile last knew — a regression the
restoration itself introduced, and only for the tray.
`record_selected_node` is the backend counterpart of the frontend's
`useRecordSelection`. Failing to record does not fail the switch: the core
is already on the new node by then, and reporting the switch as failed
would be wrong. It is logged instead.
* fix(core): restoring selections must not prune what the core has not loaded
Putting the profile's selections back when a core starts reused the
reconciliation written for profile switches, and that one also *repairs*:
a record whose group is absent from two snapshots a second apart is
dropped, and the reduced list is written to profiles.yaml.
After a profile switch the configuration is loaded, so absence is
evidence. After a core start it is not — a provider-backed group can be
empty or missing for seconds — and a record pruned on that evidence is
gone for good, because the provider finishing later cannot bring it back.
Restoring now keeps the records and only applies what it can match;
pruning stays with the caller entitled to conclude it.
The restore is also asynchronous: it reads the profile, polls the core
until its groups are readable, then puts its selections. A choice the user
makes inside that window is newer than what it captured, so letting it
finish pushed the core back to the older node while the profile already
held the newer one. Recording a selection now supersedes any activation in
flight — from the tray, and from the UI and chain proxy through
`patch_profile`.
* fix(core): activate the selections of the profile that is actually running
The activation worker read the profiles draft. A profile switch stages its
target there before validating it, so a switch that fails — leaving the
previous profile running — could still have handed an activation the
rejected profile's selections, which it would then apply to whichever
same-named groups the running profile has.
Reading the committed state removes the window. The switch that succeeds
commits before it activates, so nothing changes for that caller; what
changes is that a restore running at core start can no longer observe a
switch that never happened.
* fix(core): stop a restoration from undoing a choice made while it runs
Two gaps in making the profile authoritative, both found by adversarial
review of the commits that introduced it.
A restoration reads the profile's selections once, then polls the core
until its groups can be read — seconds, on a cold start. A selection made
inside that window is newer than what the restoration is carrying, and
nothing told the restoration so: it went on to apply the older choice over
the user's. The UI accepted the new node and traffic snapped back to the
old one, with the profile and the running core then disagreeing. Recording
a selection now abandons any restoration still in flight, through the same
generation counter every other superseding caller uses.
The other gap is the home card in global mode, which passed
`skipConfigSave` and so recorded nothing. That was harmless while nothing
re-applied the profile; now it means the next start undoes the choice. It
was the only caller that ever set the flag, so the flag is gone rather than
flipped — a parameter whose one use was a bug is not worth keeping.
* fix(core): wait for the selections that can be put back, keep trying for the rest
Two gaps in restoring the profile's selections when a core starts, both
found by adversarial review.
Restoring re-checked once, a second later, and stopped. That is enough
after a profile switch, which happens with the configuration already
loaded. A core start is the opposite case: a provider-backed group is
present but empty until its provider finishes loading, and a `select` can
fail while the core is still settling. Neither produces an activation, so
the restore reported success having applied nothing and the group stayed
on the first entry of its `proxies:` list — the outcome restoring exists
to prevent. It now keeps looking, on a deadline, at what the core is
actually on rather than at what it managed to send.
Restoring was also spawned, and the caller enables the system proxy the
moment the start returns — so traffic could be pointed at a core still on
the wrong node. The first pass is now awaited, bounded, and the rest keeps
settling in the background: a core that will not answer delays a start
rather than blocking it, because being proxied through the wrong node
beats not being proxied at all.
TUN is deliberately not covered, and cannot be by anything here: it is
configured in the core's own file, so it carries traffic from the moment
the process starts. What puts a TUN user on the right node from the first
packet is the core's own `cache.db`, which is why the service keeps an
owner's runtime generation across restarts.
`supersede_selected_activation` and `cancel_selected_node_activation` were
the same function under two names, added independently for the same
reason, and `patch_profile` called both. One survives, called once, after
the profile is written — the only placement that needs to exist, since an
activation that read the older profile is cancelled by it and one starting
afterwards reads the newer one.
* fix(core): record a selection by naming it, not by resending the list
Recording a node selection sent the whole selection list, built from the
profile the frontend had rendered. Two selections made before that list
refreshed were therefore built from the same stale snapshot, and the later
write dropped the earlier group's choice. Since a core start re-applies
whatever the profile holds, the dropped one came back on the next restart
— the profile and the running core disagreeing, in the direction the user
would least expect.
The tray already avoided this, because it went through
`record_selected_node`, which merges one group into the profile inside
`with_data_modify`. Exposing that as a command and pointing the frontend
at it leaves one way to record a selection instead of two, and removes the
list from the wire: what a caller knows is the group and the node, and
that is now all it says.
`useRecordSelection` no longer reads the profile at all, which also makes
it stable across renders rather than changing identity whenever the
profile does.
* test(core): make every way of declining to stage answer for itself
`CoreRestarted` was added to `StageRejection` upstream and reached
production without a test. It inherited the right verdict — every
`RestartRequired` is matched together — but nothing said so, and nothing
would have noticed if it had inherited the wrong one.
The list of rejections now goes through a wrapper whose `match` is
exhaustive, so the next variant added upstream fails to compile here. That
is the part worth having: it does not prove the list is complete, but it
does mean nobody can add a way to decline without being sent to this test
to say what it should lead to.
* build(deps): pin the service protocol to v2.6.1
The pin tracked `feat/stage-runtime` because the protocol change it
carried had no tag yet. It has one now, and a branch pin makes the build
irreproducible: an upstream force-push silently changes what this
compiles against.
v2.6.1 carries the runtime generation an owner keeps across restarts,
which is what lets the core hold on to the `cache.db` its node selections
and fake-ip leases live in. The protocol itself is untouched — epoch,
revision and both compatibility floors are byte-identical to v2.6.0 — so
this is a fix release in both directions: an older service still works
with this app, and this app still works with an older service, only
without the durable generation.
The lockfile change is the two lines that name it. Reaching for
`cargo update -p` instead re-resolved a subtree and quietly downgraded
`windows-sys`, `windows-core` and `getrandom` along the way.