* feat!: remove signal crate
* fix: clippy
* feat(signal): limit the shutdown signal crate to Linux
Upstream now covers termination and session-end events on Windows and
macOS, but Linux has no equivalent, so pre-shutdown cleanup there still
depends on listening for SIGTERM/SIGINT/SIGHUP (#7827). Restore
clash-verge-signal behind a target_os = "linux" gate instead of dropping
it: the dependency lives in a linux-only target section, the crate root
carries an English Linux-only rationale and compiles to nothing
elsewhere, and the Windows backend is gone.
register() now takes the quit flow's unit future: the latch serializes
in-flight cleanups and releases after each attempt, leaving the
exit-or-cancel decision to the quit flow itself.
Verified with cargo check and clippy for aarch64-unknown-linux-gnu, and
cargo check on the host where the crate compiles empty.
* 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>
* chore(deps): update cargo dependencies
* build(deps): revert rfd from 0.17.2 to 0.16.0
- This also prevents Mend Renovate from auto rebase
Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
* fix(nsis): include missing Nullsoft headers
- Transitive macro named `RestartManager_StartSession` cannot be found
during NSIS installer generation
- The macro comes from "RestartManager" plugin and should be explicitly
included in the `installer.nsi` file
Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
---------
Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Translate the 174 frontend strings and 8 tray/notification strings that were
still English copies in the Turkish locale. Values only; no keys changed.
Co-authored-by: qwist1233-cpu <qwist1233-cpu@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(log): migrate logging to tracing-estuary pipeline
Swap flexi_logger + clash-verge-logger wiring for the tracing-estuary
crate: buffered file sink with explicit panic/exit drains, live level
switching, and tokio-trace/chrome-trace behind crate features.
AsyncLogger moves in-house (VecDeque ring); file format and rotation
stay unchanged.
* fix(log): reset sidecar writer in place and re-pin service-ipc v2.6.7
Rebuilding the sidecar FileLogWriter on config changes leaked flexi's
unstoppable flusher thread plus an fd each time; the writer is now flushed
and reset in place, matching what the service crate does.
get_clash_logs_by_service drops the CompactString conversion layer now that
service-ipc v2.6.7 returns plain Vec<String>, and AsyncLogger is renamed
LogRing to match.
* refactor(log): span-based observability and log noise cleanup
Instrument spans carry the structure flat lines never could: the core-start
spine (mode, readiness/owner generations, pid, config file), proxy apply and
clear routes with guard generations, service IPC sessions with refusal codes
and outcomes, profile-switch terminal outcomes, subscription fetch strategies,
core upgrades with per-proxy version probes, backup flows, timer tasks, config
generation uids, and the startup setup flow; running-mode changes emit one
guarded transition event each.
~90 hand-written noise lines are gone or demoted (entry+success pairs, polled
getter echoes, phase brackets, window sub-steps, platform install chatter);
errors keep their anyhow chain, operation name, and item id, and swallowed
retry results surface per attempt.
* fix: Gemini test
* naming unlock
* test type
* refactor: deduplicate media unlock checks
* fix: run media unlock checks individually
* fix(media-unlock): satisfy clippy const lint
* fix: stream media unlock test results
* fix: streamline media unlock checks
* fix: force HTTP/1 for media unlock checks
* fix: limit concurrent media unlock checks
* feat: allow concurrent media unlock item checks
* 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): reopen PAC when a start attempt never happens
`save_proxy_ports` closes the PAC endpoint before it stages a candidate,
because the endpoint resolves the Mixed Port through the draft layer and
would otherwise answer with a port nothing is listening on.
Most ways out of that function are a rejection, though: the candidate is
refused and the Core keeps serving on its old ports, having never been
stopped. PAC is re-derived only when the Running Mode changes, and that
is exactly what did not happen — so the endpoint stayed shut for the rest
of the session, and every browser on PAC silently went direct.
Give `core_starting` the counterpart it was missing. `core_start_settled`
re-derives PAC from the Running Mode rather than restoring a saved flag,
which makes it correct on all three endings — a start that succeeded, one
that was abandoned, one that failed and left the Core down — and therefore
safe to run from a guard on every path.
* fix(config): refuse overlapping draft transactions instead of losing an edit
A layer holds exactly one draft, so a second writer's `edit_draft`
overwrites the first writer's staged value in place. `DraftTransaction`
nonetheless rolled back by calling `discard` on the shared layer, which
discarded whatever was staged there — including a change another
transaction was about to commit.
The other transaction then found no draft, and `apply` on an absent draft
does nothing at all. It saved the committed value, returned Ok, and its
side effect had already happened: auto-launch registered with the OS while
the config that describes it says otherwise, and no log line either way.
"Roll back only my edit" cannot be implemented over a single slot, so
enforce the exclusivity the type already assumed. Beginning a transaction
claims every layer and refuses rather than interleaving, which is the same
shape as `try_start_config_update` and `begin_operation` elsewhere.
`patch_verge` is the caller that matters: it holds its draft across
`process_terminated_flags`, which restarts the Core and can take seconds.
The TUN reconciliation runs on every Run State transition and can now lose
that race, so it treats a busy layer as "later" rather than as a failure
worth telling the user about.
* fix(core): reconcile TUN availability when the setting itself changes
Moving the TUN availability guard from the frontend into Rust also moved
what triggers it. The old effect depended on `enable_tun_mode`; the new
reconciliation is spawned from `RealEnv::publish`, so it only runs on a
Run State transition. Those two sets are not the same, and the setting
itself falls in the gap.
Switching TUN on from the global hotkey is the case that reaches it. That
path is not gated on availability — unlike the tray item and the settings
switch, both of which read `tun_capable` — and the flags a TUN patch
raises are answered by a Core reload, which changes no Run State at all.
So on a machine with no Service and no elevation the setting stayed on:
tray icon, settings switch and `enable_tun_mode` in verge.yaml all
reported TUN as enabled while nothing carried its traffic, until some
unrelated core lifecycle transition happened to reconcile it.
Reconcile after any patch that touched the setting. The reconciliation
writes configuration itself, so it goes through the plain apply rather
than back through the patch-and-reconcile form; the absence of a cycle is
then a property of the call graph rather than of a runtime early-return.
`toggle_tun_mode` now reads the setting back instead of returning what it
asked for, so the hotkey cannot report TUN as on moments before the notice
saying it was turned off.
* fix(core): an action already under way is not a decision to put to the user
The legacy status could not distinguish a requested privileged operation
from one already running, because readers blocked until the operation
finished and so never observed the request at all. Run State pushes every
transition instead, and `service_needs_attention` treated any pending
action as a question for the user.
So an install started from the settings page or the home card raised the
migration dialog on top of it, and an uninstall raised one offering to
reinstall the Service being removed. Both offer a button that can only
fail, since the privileged-operation slot is already taken.
An operation in flight is a report of progress. The dialog the user is
already driving keeps itself open through its own loading state.
`tun_should_be_disabled` is unaffected: it already refused to act while an
operation was in flight, so the term this adds is redundant there.
* fix(core): let the snapshot decide whether the Run State has settled
`settled` read the operation slot twice: once to decide whether to wait,
then again inside the snapshot it returned. An operation claiming the slot
between those two reads was described rather than waited for.
`prepare_startup` reads this. A start racing an install would see the
requested install, flatten it to `InstallRequired`, and decide `Wait` —
leaving the core stopped where waiting for the install to finish would
have told it whether the Service came back.
Read the slot once, inside the snapshot, so there is no window rather than
a second check closing it. The rewrite that introduced this also fixed a
lost wakeup here, and that fix stands: the waiter is still registered
before the state is read.
* fix(ui): re-read the Run State once its listener is live
Replacing the poll with a push left a gap. `listen` resolves
asynchronously, so between the first `get_runtime_state` and the
subscription being registered there is a window, and a transition landing
in it is not queued anywhere — it is simply missed. The next event is
whenever the backend happens to move again, which during startup can be a
long time; the 30-second safety net was all that stood behind it, where
the old code polled every two seconds precisely through startup.
The Run State is the one thing here that arrives only by event, so it is
the one thing a missed event leaves stale. Re-read it once every listener
is live, which closes the gap without going back to polling for it.
`onSubscribed` fires even when a registration failed, because a resync is
most needed when a subscription is missing, and not at all after teardown.
* docs(config): state what makes the PAC endpoint's draft read safe
`MixedPort::desired` reads the draft layer, so the PAC endpoint can name a
port the Core has not moved to yet. That is only safe because the one path
staging a listener port closes PAC across the change. Nothing said so at
either end, and the two are in different modules.
* fix(core): ask again when staging goes silent rather than assume it failed
The Service commits the generation before it answers, so a reply that never
arrives is not evidence that nothing was written: config.yaml can already hold
the new configuration while this side concludes the staging never happened. The
Core keeps running the previous configuration, so nothing looks wrong -- until
the Core dies and the Service's watchdog restarts it in the same generation from
whatever config.yaml says, quietly making live a configuration the app reported
as failed.
Silence is the one answer that has to be resolved rather than acted on, so ask
once more before falling back. It is cheap exactly where it matters: staging
plans against the manifest it wrote, so a bundle that already landed copies
nothing and answers in milliseconds, and the Service's lifecycle lock makes a
second ask wait for a first one still in flight instead of interleaving with it.
A deadline keeps that wait off the caller, and a second silence goes where the
first one used to.
* refactor(core): introduce core::runstate as the owner of Run State
"Is the Service available?" had five caches and three predicates, and
prepare_startup consulted all three within thirty lines. This lands the
foundation for collapsing that, with no behaviour change: the old owners
now delegate into one store.
The state model splits observation from intent. Five of ServiceStatus's
ten variants were never observations — cmd/service.rs passed
ServiceStatus::InstallRequired as the "which operation do you want"
argument — which is why narrowing to ServiceInstallState had to be lossy.
ServiceHealth now records what we saw, PendingAction records what the
session asked for, and ServiceStatus::from_run_state flattens the pair
for call sites that still expect the single slot.
Reads differ only in freshness: state() is cached, settled() waits for an
in-flight privileged operation, probe() forces IPC. What an answer means
lives on the snapshot (service_usable, tun_capable), so callers stop
writing their own formulas — the tray now asks tun_capable() instead of
recomputing "admin or ready".
Effects sit behind RunStateEnv with two adapters, RealEnv and FakeEnv.
That retires the seven tests that hand-built a private ServiceManager
struct literal, and makes the probe and detection paths testable for the
first time.
Two distinctions are now explicit rather than incidental:
- A single failed probe is not an observation and leaves confirmed
health intact; exhausting a poll's whole retry budget is one and
records unavailable. Both are tested, protecting 389c5342.
- Platform evidence that cannot be inspected is unavailable, not
absent, so we never offer to install a Service that is already there.
RealEnv::is_elevated reads APP_HANDLE without expect(), so reading Run
State before the app handle exists degrades to "not elevated" instead of
aborting.
Tests: 180 -> 218. One test was removed; its assertions moved into
runstate::probe.
* refactor(core): derive PAC and the core-mode mirror from Run State
Running Mode had twelve write sites, PAC availability ten, and the
frontend mode mirror seven — all three kept in step by convention, each
pairing written out by hand next to the others. Nothing checked the
pairing, so a new call site could set a mode and leave PAC serving a
script for a proxy port nothing was listening on.
Callers now state what happened rather than what to store:
core_started(Sidecar|Service) the Core is running and serving
core_stopped() the Core is not running
core_starting() a start attempt is under way
PAC availability and the mode mirror are derived from those transitions
inside the store and nowhere else, so they cannot drift. set_mode is
private; after_core_process is deleted along with all seven of its calls.
Ordering is fail-closed in both directions: PAC closes before the mode
changes and opens after it, so a reader that catches a transition
mid-flight is refused rather than handed a stale script. core_starting
exists for exactly that window during a handover, where the Core being
replaced is still the one that holds the mode.
Both effects sit behind RunStateEnv, so FakeEnv records them instead of
touching process globals, and the derivation is asserted directly.
RealEnv publishes the mirror without expect(), so a Core that stops
before the app handle exists no longer risks an abort.
utils::server's fail-closed test now asserts the default rather than the
live flag: PAC follows the Running Mode, so any test driving a Core
transition legitimately moves it. The invariant that PAC can never
disagree with the mode is asserted in core::runstate instead, where the
environment is a fake.
Tests: 218 -> 223.
* refactor(core): absorb the service wait and owner monitor into Run State
Four loops asked the same question — is the Service there? — with four
retry policies, and the one that mattered most was the least examined:
112 lines of owner monitor with three copy-pasted counter blocks and no
tests, where a missed reset silently changes when the app tears down a
working proxy.
The readiness wait becomes one interface. Its two callers wanted
different things from a failed wait, and the old code expressed that
implicitly in whether a poll returned Ok — a poll returned Ok for a
readable-but-incompatible reply, which is why one caller recorded
"unavailable" and the other did not. ReadyWaitError now says which
happened: silence is the caller's to interpret, a rejected reply already
recorded its own verdict and must not be flattened into a vaguer one.
Retrying is likewise only for silence; twenty retries of a known-bad
version would just delay telling the user to reinstall.
The owner monitor's decision moves to core::runstate::owner as a state
machine over samples. A transport error, an error code and an empty
payload were three near-identical blocks; they are one Unreadable sample
now, because they mean the same thing: we learned nothing. The two
counters are separate and independent — silence says nothing about the
Core's PID — and VerifyTransport keeps the rule that a Service we cannot
reach while the Core still answers is a broken status channel, not a
lost Core. 15 tests, where there were none.
Privileged operations run through the environment, so the state around
them is testable: a refused uninstall leaves the Service untrusted
rather than absent, and a refused install does not claim it is gone.
That also retires PrivilegedServiceAction, which duplicated
PendingAction — apply_service_status now reads back the one mapping
set_status already performed instead of re-deriving it.
Fixes a test-isolation regression from the first stage: moving Running
Mode out of CoreManager's per-instance State made three tests share one
process-wide mode, which flaked once during this work. CoreManager now
holds a reference to its Run State, so a test can supervise a Core
without racing every other test.
Tests: 223 -> 241, verified stable over repeated runs.
* test(frontend): wire vitest and revive the orphaned proxy-view tests
There was no frontend test runner, and tests/proxy-view.test.mjs — 421
lines covering the proxy view's identity and rebinding rules — was
referenced by no script and no workflow. Unsurprisingly it had rotted:
three of its ten tests called functions that no longer exist.
vitest runs it now, and CI runs vitest. The file keeps
node:assert/strict so every surviving assertion means exactly what it
did before; only the harness import changed.
Of the three broken tests, one was salvageable: rebindNode is still live
and the deleted toNodeBinding was just { name, source }, so inlining it
restores the test unchanged. The other two covered findGroup and
selectRuntimeStandaloneNodes, which are gone; they are deleted rather
than guessed at. If the behaviour they described still exists under
another name, it is currently uncovered.
The revived file is deliberately *not* type-checked. It was written as
untyped .mjs — widened literals, runtime asserts that do not narrow —
and retyping it would mean rewriting assertions I am trying to preserve.
It is registered with ESLint's default project instead. New tests belong
under src/**/*.test.ts, which tsconfig already covers, so they get full
type checking from the start; untyped tests are how this one rotted.
Only vitest is added. Testing-library comes with the first hook test
that needs it, so knip does not flag an unused dependency in between.
* refactor(core): one Run State snapshot across the IPC seam
Three commands described one fact — running mode, service install state,
elevation — under three query keys with three refresh intervals, and the
frontend kept them coherent by writing two caches by hand after every
service action. get_runtime_state replaces all of it with one snapshot
under one key.
The snapshot carries the derived answers, not just the fields:
tunCapable, serviceUsable and serviceNeedsAttention are computed in Rust
and travel with it. That is the point — "TUN can work" had three
definitions across the tray, the startup path and a React hook, and the
migration dialog decoded an enum ladder to decide whether to open.
Transitions are pushed. Every Run State change emits
verge://run-state-changed carrying the new snapshot, which the frontend
writes straight into the cache instead of refetching. Polling drops from
2s-during-startup to a 30s safety net, and the startup grace period goes
away: the service reports 'unknown' until it has actually been probed,
so the guard no longer has to wait out a fixed 10s to know whether to
trust the answer.
ServiceInstallState is deleted along with the lossy install_state()
narrowing that motivated splitting observation from intent in the first
place. So is the isAdmin wrapper — elevation is part of the snapshot now.
app_core_mode is deliberately kept: it is not a redundant mirror, it
feeds export_diagnostic_info's "Running Mode" line. The publish effect
now carries the whole snapshot and updates both it and the frontend, so
the two cannot disagree.
Rust tests 241 -> 249, covering the derived answers and that every
change — health, pending action, mode, and both edges of an in-flight
operation — is announced exactly once.
* refactor(core): decide TUN availability where the knowledge is
Turning TUN off when it stops being possible was a React effect: six
conditions, a ten-second startup grace period, a one-second cooldown ref
and a triple-negative predicate, guarding a write to the user's config
file — and none of it covered by a test, because until now the frontend
had no way to run one.
The grace period and the cooldown were both approximations of things the
backend actually knows. "Has the service been probed yet" is now
ServiceHealth::Unknown rather than a ten-second guess, and "is a disable
already being written" is a flag rather than a timer.
RunState::tun_should_be_disabled is the decision and is pure, so the
cases that must *not* trigger a write are pinned: an unprobed service,
an operation in flight, and a decision still sitting in front of the
user all mean not-yet-known. Writing the config is a separate concern in
feat::tun, because Run State owns state and not actions.
Behaviour is unchanged from the user's side: TUN is still switched off
in verge.yaml, with the same two notices, which now travel over the
existing notice channel as tun_mode::auto_disabled.
Worth knowing for review: this puts a config write on the Run State
transition path, and patch_verge's bitflag mapping has no tests of its
own. The write is guarded against the cascade it can trigger — patching
the config produces further transitions, each of which reconciles again
— and is spawned so a transition never waits on it.
One of the new tests caught a wrong assertion I had written: a ready
service mid-operation looked like "cannot work" but is really "not yet
known". That is the exact class of bug the fixed grace period was
papering over.
Rust tests 249 -> 256.
* refactor(config): name the two questions asked of the Mixed Port
Six call sites hand-copied "the user's selection, else the Merge Config"
and the frontend hand-copied a different chain that prefers what the Core
reports. Reading them as one duplicated rule is what made this look like
a straightforward dedup; they are actually two questions.
What have we configured? — needed by anything that writes config or
decides what to start, and answerable before the Core exists. What is the
Core serving on? — needed by anything pointing a client at the proxy,
because a startup fallback or a hand-edited Core config moves the port.
MixedPort::desired and MixedPort::effective say which is which, and each
call site now picks one on purpose.
Two defects fall out of naming them. feat::proxy built the proxy command
the user pastes into a shell from verge-or-a-hardcoded-7897, ignoring the
Merge Config entirely — it wanted effective(). And the six chains split
three-to-three between reading the committed and the draft config layer,
with feat::clash reading one of each, so mid-edit they disagreed; there
is one layer now.
effective() costs a round-trip, so it is used where a user triggered the
work, not on paths served per request. PAC stays on desired(): it answers
per browser request, and it is closed while the Core is between ports
anyway. Each site says which it chose and why.
The reserved-ports list in the startup fallback was a second hardcoded
listener table. It is derived from the first now, filtered to exclude the
Mixed Port itself — which is not a duplicate but a deliberate omission,
so it is stated as one rather than left implicit in a literal.
On the frontend the same four-source merge was computed twice, once in a
hook and once inline in the provider. The provider was mounting two
queries solely to recompute what the hook already had; both are gone.
Rust tests 256 -> 262.
* refactor(config): make a config change roll back by itself
Editing configuration meant pairing every edit_draft with an apply or a
discard on every exit path, by hand, across three independently
committed layers. The startup port fallback wrote the same three-line
rollback five times, save_proxy_ports called its rollback helper from
eight branches, and apply_config's nested match tree had six discards
and three applies. Nothing checked the pairing: a new early return that
forgot one left the app describing a configuration it was not in.
DraftTransaction commits a set of layers together and rolls them back
when dropped, so an early return — a ?, a bail!, a panic — cannot leave
a change half-applied. Committing is the explicit act, deliberately:
forgetting to commit costs a retry, forgetting to roll back corrupts
state.
The three call sites lose their hand-written rollback and gain ordinary
control flow. apply_config stops touching drafts entirely; whether the
staged Runtime Config survives now follows from whether applying it
worked, which is the caller's transaction to decide, and its nested
matches flatten out.
Two rollbacks stay explicit and are commented as to why. Files on disk
and a restarted core are not part of the transaction, so those paths
still restore snapshots themselves; and rollback_proxy_ports has to
clear the drafts *before* it regenerates files from the committed
config, which is earlier than drop would.
Draft discards drop from 14 sites to 8, applies from 18 to 15. The
crate gains 4 tests, including that an early return rolls back without
the caller saying so.
* refactor(proxy): state the delay sentinel rules once
A delay from the core is not just a latency: -2 means a test is running,
other negatives mean never tested, zero and anything past the timeout
mean it timed out, and absurd values mean error. Every surface agreed on
those rules and restated them, and the two sort comparators had already
drifted — a non-numeric delay sorted among the timeouts in the proxy
list and after the sentinels in the home card.
classifyDelay and compareByDelay say it once, with tests, including the
case the two copies disagreed on. The trap the ranks exist for is worth
naming: zero encodes a timeout, so sorting delays as plain numbers puts
the nodes that failed at the top of a fastest-first list.
Deliberately not unified: how a *measured* delay is graded. A
three-colour chip and a four-bar signal icon draw different
distinctions, and collapsing 400/250 into 500/300/200 would be inventing
a UI decision rather than removing a duplicated one. Those thresholds
stay where they are, next to a comment saying they are a presentation
choice. The report that prompted this called them "4 incompatible
threshold sets"; on reading them, the sentinel half was duplication and
the grading half was not.
ProxySortType was declared twice; it is imported now.
Frontend tests 8 -> 21.
* refactor(enhance): make the app's authoritative fields one value
The pipeline derives fields the app owns — ports, the external
controller, the tray-driven mode — then lets the user's merge and script
overrides run, then puts its own values back. That ordering is a
correctness requirement: capture must follow the app's own stages and
precede every override, restore must follow all of them. It was four
separate calls held together by four comments.
AuthoritativeFields captures and restores as one value, so the restore
cannot be forgotten, reordered ahead of the overrides, or applied to a
config the snapshot did not come from. Five tests state what it is for,
including the two cases that are easy to get backwards: an override must
not re-add a field the app deliberately left out, and reinstating
dns.ipv6 must not conjure a dns block an override removed.
merge_default_config now takes enable_external_controller instead of
reading it out of the global config from inside its loop. A stage that
looks like a function of its arguments and is not is worse than one that
admits it, and this is the change that makes it testable at all.
Not done here: reshaping the sixteen steps into a stage list. The value
in this pipeline is the nine steps with no tests, not the shape of the
function that calls them, and the two contracts above were the parts
that could silently break.
Rust tests 262 -> 267.
* refactor(proxy): make the proxy page's outcome a closed set
resolveProxyEmptyState returned `reason | 'loading' | null`, which lied
twice: 'loading' is not a reason and ProxyEmptyState cannot render it,
and `null` meant either "there is content" or "this is direct mode" —
so the caller decided direct mode a second time, before even asking.
resolveProxyListState returns one closed set of outcomes and the caller
switches on it. Ten tests pin the ordering that was previously implicit,
including that anything still loading outranks every verdict about
emptiness, and that global mode asks about GLOBAL rather than groups.
Known gap, left alone deliberately. In chain mode the model asks whether
any Selector or URLTest group exists, while the renderer builds its list
from selectRuleChainMembers or selectGlobalChainNodes — unrelated
questions, and both a false "empty" and a false "not empty" are
reachable. Fixing it means the model needs the selected group and the
runtime proxies, which is the fold of emptiness into use-render-list
that this candidate was really about; guessing at it from outside would
add a third derivation rather than remove the second.
Frontend tests 21 -> 30.
* refactor(events): one subscription with one teardown
Four places listened for backend events, three idioms between them, and
four separate implementations of the same cleanup. One of them —
app-data-provider — pushed each unlisten onto its cleanup list only
after awaiting registration, with no disposed flag, so a subscription
that resolved after unmount was never torn down. That is now handled in
one place, because registration being asynchronous is a property of the
mechanism rather than of any call site.
Event names were string literals on both sides of the IPC seam, so a
rename was a silent no-op. The frontend now declares each name once with
its payload type, which does not make Rust check itself but does replace
a per-call-site `listen<T>` guess with one place to look.
use-layout-events loses about fifty lines that were registration
plumbing rather than behaviour, and profiles.tsx loses a
Promise.allSettled that existed to collect unlisteners.
Left alone: window events — onResized, onFocusChanged,
WINDOW_CLOSE_REQUESTED. They come from Tauri's window API rather than
the app's own bus, and folding them in would widen the registry to
things it does not name.
* fix(ui): stop AppDataProvider consuming its own context
The provider rendered nothing and threw "useClashConfigData must be used
within AppDataProvider": a previous commit replaced its inline mixed-port
merge with useDisplayedMixedPort, and that hook reads the ClashConfig
context which this component is the one providing.
The provider resolves the port from its own sources again. The two
computations are not the duplicate they looked like — one runs inside the
provider and one outside it, and a hook that reads the provider's context
cannot run in both places. They share resolveDisplayedMixedPort, which is
the part that was ever worth sharing; a comment now says why the hook is
not used here.
Nothing else in the provider consumed its own context: it imports the
Context objects rather than the reader hooks, and useClashInfo and
useRuntimeConfig are plain queries.
* fix(core): repair regressions found by review
An independent review of this branch found several defects, two of them
introduced here and reachable on a normal first run.
Startup could silently turn TUN off. Service health is observed as
NotInstalled before the startup path decides whether to install the
service, and the TUN reconciler ran against that captured snapshot — so
it wrote enable_tun_mode: false to the user's config, and because
prepare_startup reads that same setting, the install prompt that would
have fixed the situation never appeared either. The deleted frontend
guard covered this with a ten-second grace period; nothing replaced it.
The reconciler now re-reads the current Run State instead of taking a
snapshot, and refuses to act at all while the Core is not running,
because until then no decision has been made to act on.
It also overrode a decision that had already been taken. Falling back to
Sidecar calls suppress_tun_for_session, which is deliberately not
persistent — disable_tun_and_persist exists as the separate explicit
act. The reconciler now honours the session suppression rather than
escalating it to a config write.
Reconciliation was additionally skipped whenever the app handle did not
exist yet, because it was spawned after that early return; it is spawned
before it now, which is safe since it no longer depends on the snapshot.
apply_service_status wrote the requested action into the store and read
it back to decide what to run. A concurrent observation clears the
pending action, which would have turned a user-authorised install into a
silent no-op. It derives the action from its own argument again, as the
code it replaced did.
save_proxy_ports opened its transaction before its first fallible read,
so that early return discarded drafts staged by other code that this
function never touched. It also left rollback to drop, which runs after
the recovery paths that regenerate files from committed config — and a
second time after those paths had already discarded. The transaction now
opens after the read and is rolled back explicitly where recovery needs
it to have happened already; the same ordering fix applies to the
startup port fallback. DraftTransaction gained a consuming rollback for
this.
Two pre-existing defects the review surfaced are fixed here because this
branch's code depends on them. patch_verge's `?` returned past its own
`if let Err(..) { discard() }`, which the compiler could never reach, so
a failed patch left the edit in the draft — and the TUN reconciler would
then read that draft, believe TUN was already off, and never retry.
settled() built its Notified future but never registered it before
checking the flag, so an operation finishing in that window was a wakeup
lost for good; the waiter would then block until some unrelated later
operation, or forever.
Deferred with reasons: cancellation-safety of the config futures (no
cancellation site demonstrated), enter_mode's PAC/mode atomicity
(pre-existing, narrow), and the draft-vs-committed reads behind
MixedPort::desired (the PAC window is now closed by core_starting at
staging time).
Rust tests 267 -> 268.
* docs(core): inline the boundary rationale into the code
The comments pointed at a glossary and an ADR for why Run State owns
state but not Core lifecycle. The reason is short enough to say in
place, and a reason that travels with the code it constrains is worth
more than a pointer to a file a reader has to go and find.
* test(ui): mount AppDataProvider so a context misuse fails loudly
A provider that reads a context it supplies throws during render and
blanks the whole UI. That shipped: AppDataProvider called
useDisplayedMixedPort, which reads ClashConfigContext. typecheck, lint,
knip and web:build all passed with it present, and so did an independent
three-reviewer code review — the frontend reviewer returned nothing on
it. Only rendering can see it.
The test stubs `invoke` and `listen` and nothing above them. Everything
the provider mounts — the SWR queries, the config hooks, the event
subscription, the mihomo plugin — goes through those two, so all the
real application code still runs. Stubbing the hooks instead would have
produced a test that could not catch what this exists to catch.
Verified by reintroducing the bug: both cases fail with the production
error, "useClashConfigData must be used within AppDataProvider". A smoke
test that passes either way is worth nothing, so this one was checked
against the defect rather than only against a working tree.
Render tests opt into jsdom per file, so the DOM cost is paid only where
it buys something; the rest of the suite stays on node. The libraries
land in this commit rather than ahead of it, because knip fails on a
dependency nothing uses yet.
Frontend tests 30 -> 32.
* refactor(proxy): let the list say whether it is empty
The empty-state model predicted "is there anything to render" beside the
renderer instead of asking it. In chain mode the two asked different
questions: the model looked for any Selector or URLTest group, while the
list is built from selectRuleChainMembers or selectGlobalChainNodes.
Both a false "empty" and a false "not empty" were reachable — GLOBAL
with standalone nodes but no Selector group showed nodes while the page
said no-proxy-info, and Selector groups with no global and no standalone
nodes said content over an empty list.
Emptiness is now an observation. hasRenderableItems reads the list that
is about to be drawn, and the two list components render the empty state
themselves when it comes back with nothing. resolveProxyListState keeps
only what is knowable without building a list — direct mode, still
loading, and having no usable subscription — which drops proxyView and
isChainMode from its input and leaves it with one job.
Two details worth stating rather than leaving to be rediscovered. A
group header counts as content only when the group is visible, matching
what the old predicate meant by "some group is not hidden". Rows inside
a hidden but expanded group now count, where the old predicate called
the page empty; the user can see those rows, so claiming otherwise was
a lie.
Frontend tests 32 -> 38, including the chain case that was wrong in both
directions.
* test(enhance): cover the config transforms that had none
Seven of the pipeline's steps had zero references from any test. Four of
them are pure functions of their arguments and are now covered; the
other three are not, and the difference is the point.
use_tun, ensure_fake_ip_range6, use_sort and merge_default_config decide
what ends up in the file the core reads, and each has behaviour that is
easy to get backwards and impossible to notice:
- a disabled listener has its port *removed*, not merely skipped, so the
profile's own value cannot open a listener the user switched off
- a disabled external controller is blanked rather than deleted, because
an absent key lets mihomo fall back to its default and listen anyway
- an empty fake-ip-range6 counts as missing, since YAML edited by hand
leaves the key present with nothing after the colon
- a deliberate redir-host DNS setup is left alone; only fake-ip is ours
- turning TUN off does not rewrite DNS, only the system resolver
- sorting drops nothing, and is stable when applied twice
get_config_values and collect_profile_items are left uncovered on
purpose: their whole job is reading global config, so a test would
assert that a mock returns what the mock was given. apply_dns_settings
and apply_builtin_scripts read the filesystem and run JS respectively;
covering those means giving them a seam, which is a change to the code
rather than a test of it.
Rust tests 268 -> 289.
* refactor(proxy): re-sort when a delay test settles
Sorting a group by delay read the delay store inside a memo that had no
dependency on it, so the order only refreshed when something else forced
a recompute. Both places that sort by delay had their own way of forcing
it, and both hooked only the "test all" path: retesting a single proxy
updated that row's own number and left it sitting in the wrong place.
The fix is a subscription, but the granularity is the whole design. The
store already coalesces notifications onto an animation frame, so the
cost was never the render count — announcing per result would re-sort
continuously while a batch is still running, with rows moving out from
under the pointer. So the group listener now means "a test settled":
once per single test, once per batch however many proxies it covered.
Measured against the code before this, a 20-proxy batch would have
announced 21 times.
That granularity is the thing that normally cannot be asserted, because
its symptom is jumpy scrolling rather than a wrong value. Moving the
trigger into the store turns it into a number a test can count, and the
batch test was checked against the defect: with per-result announcements
restored it fails with 21 where it expects 1.
The hooks hand over a snapshot rather than a revision counter. A counter
has to be listed as a dependency the memo never reads, and
exhaustive-deps is right to reject that — the code this replaces
satisfied the rule with a `refreshTick >= 0 ? … : 0` ternary whose only
purpose was to make the counter look used. A snapshot makes sorting a
pure function of a value it is handed. Both invalidation hacks and that
ternary are gone.
Group listeners are a set now, each with its own unsubscribe. They were
one-per-group, so two views watching the same group would have silently
replaced each other.
Frontend tests 38 -> 43.
* fix(proxy): repair the delay reactivity found by review
A review of the previous commit found four defects, two of them worse
than the code they replaced.
The snapshot hook returned a new object on nearly every render. Its
state started keyed to a null group, and in the home card the group name
resolves after the first render, so the derived branch was taken until
some test settled — which, with auto-testing off, is never. The sort
memo therefore re-ran on every render and handed a new array downstream.
The counter it replaced was at least stable.
Both hooks also read during render while the store notifies from an
animation frame, so a settle landing between render and the passive
effect that subscribes was dropped with nothing to re-trigger it. Both
are now built on useSyncExternalStore, which is the mechanism for
exactly this and fixes the identity churn at the same time: the store
hands out a per-group handle whose identity changes only on settle.
A single test could announce while a batch was still running. The
per-row delay button and "test all" hold separate locks, so a user can
start one inside the other, and sorting from a half-measured group is
precisely the reordering this design exists to prevent. Batches are now
counted per group and a settle inside one is suppressed until it exits —
in a finally, so a throw cannot leave the group unannounced with its
proxies pinned at "testing" and nothing later to repair the order.
One settle also invalidated every group's cached order, because all
groups shared a single token. Each group carries its own handle now, so
a test in one group no longer makes its neighbours re-sort.
Both new behaviours were checked against their absence: removing the
batch counter fails the coordination test, and the earlier granularity
test still fails with 21 where it expects 1. The review's other three
findings are left alone with reasons — TTL expiry mutating during a read
is pre-existing and needs 30 idle minutes, NUL cannot appear in a group
name from mihomo's config, and the batch-count assertion already rules
out mid-flight fires because a 500ms floor per proxy prevents them
collapsing into one frame.
Frontend tests 43 -> 45.
* feat(core): reload the core in place when switching profiles in Service mode
Switching a profile in Service mode stopped and restarted the core, roughly 1.2s
of dropped traffic every time. This is pre-existing, from #7569 rather than from
this branch: before it, apply_config had no Service branch at all.
Two independent reasons, both now addressed. The service protocol had no verb
for "change the configuration" other than stop plus start, so a companion change
adds `/clash/stage-runtime`. And config reaches the core differently in the two
modes — in Sidecar the core reads the app's own directory, in Service it reads a
directory the service materialised, with the provider paths rewritten to match.
That asymmetry, not session ownership, is why the else-branch could never simply
be shared.
apply_config now has one shape for both modes: put the configuration where the
core can read it, ask the core to reload, replace the core if that did not work.
Only the first step differs. Service mode asks the service to stage the runtime
in place and, if it did, points the core at the service's copy over the mihomo
IPC the app already has open — the app has always been able to command the core
in Service mode; only materialising the config needed the service.
Every way of not staging leads back to exactly what happened before: a service
too old to stage, a service that declined, a request that never came back, or a
reload the core refused. `plan_config_application` is the whole of that decision
and is a pure function of the attempt, so all four are unit tests rather than
things you can only observe against a live service. Verified against the defects
they target: treating a refusal as reloadable, and dropping the provider url
during collection, both make them fail.
The capability is learned once when the core starts and is held with the owner
session rather than cached globally. It describes the service instance that owns
this core, which is the only scope where acting on it is safe — a service
upgraded underneath a running core does not silently gain abilities the core it
owns was not started under. A failed protocol query reads as "no", because the
only cost is the fast path.
`collect_runtime_bundle` now also declares remote providers. It is not an
optimisation: the core will not re-fetch a provider whose file already exists, so
a download cache left from a different url keeps being served after the switch.
The service can only tell a reusable cache from a stale one if the bundle says
which url produced it.
Not done, and it gates pushing: the dependency pin still names v2.5.3, because
v2.6.0 does not exist yet. `^2.5.3` admits 2.6.0 and a local `[patch]` in
~/.cargo/config.toml supplies it, so this builds and tests here but not anywhere
else. Cargo.lock is deliberately not in this commit — it currently records a
filesystem path. Both need a follow-up once the service-ipc branch is pushed.
Nothing here has been run in the real app. The fast path has been exercised
against a real service and a real child process in the companion repository's
integration tests, and mihomo's reload behaviour was measured directly before
any of it was designed, but a `pnpm dev` profile switch in Service mode has not
happened.
* fix(core): keep a working core when the service refuses the bundle
Follow-up to the review of the staging change, on the app side.
Every service answer was funnelled into the same fallback. A service that looked at
the bundle and refused it — an asset that is gone, a destination it will not accept,
a session we no longer own — is telling us something deterministic: the fresh start
that follows materialises the same bundle and fails the same way, so stopping the
running core adds an outage to a failure that has already happened. `StageAttempt`
now distinguishes a refusal from silence, and a refusal leaves the core running and
reports why. Silence still replaces the core, because not hearing back says nothing
about whether starting would work.
This was not a regression — before staging existed the core was stopped in these
cases too — but the information to do better only exists now, and throwing it away
was the wrong default.
`collect_provider_assets` decided a provider was remote by whether it carried a
`url`. The core decides by `type`, so the two disagreed: a `file` provider with a
stray `url` was treated as remote, its local file left uncopied, and the core could
then not find what the rewritten path pointed at. It now reads `type`, and a `http`
provider without a usable url is an error rather than a silent reclassification.
Remote destinations now claim their slot in the same set local ones do. A path
claimed by both a file the service copies and a file the core downloads is refused
here rather than reaching the service, which refuses it as well — the copy would
otherwise win and both records would match from then on, pinning the wrong content
for the life of the generation.
Verified by reintroducing each defect: treating a refusal as a restart, and losing
the provider url during collection, both make their tests fail.
* fix(core): only treat a bundle refusal as a reason not to restart
Verification found the previous commit had over-applied its own idea, and the
mistake mattered.
It routed *every* non-zero service response to "leave the core running and report
the error", on the premise that a fresh start would be refused the same way. That
premise holds only for refusals about the bundle. `StageRuntime` also answers with
a code for a protocol mismatch, a malformed request, an unauthorised owner, and a
stale owner session — and for a stale session the premise is exactly backwards:
`start_clash` proposes a new session rather than presenting the old one, so
starting is how that gets resolved. Worse, the restart path is what notices
ownership was lost: `stop_core_by_service` clears the proxy and PAC and marks the
core stopped when the session is stale, and skipping it left that undone until the
owner monitor came round two seconds later. Only `InvalidRuntimeAsset` and
`InvalidInstallLocation` now leave the core alone; everything else replaces it, as
before.
`collect_provider_assets` was refusing two providers that name the same file *and*
the same url. The service folds that — it is one file with two names for it, not a
conflict — and mihomo accepts such a config; measured, not assumed. Since this
function is also where a Service-mode *start* gets its bundle, the refusal stopped
the core coming up at all. It now compares urls and refuses only genuine
disagreement.
Deciding remote by `type == "http"` was right, but failing the whole bundle when
that classification does not fit was not. Measured against mihomo v1.19.26: it
accepts a `http` provider with no url, an `inline` provider carrying both a url and
a path that does not exist, and a `file` provider whose path is missing. Each of
those had become "this configuration cannot be applied at all", where before it
merely behaved oddly. A provider that cannot be classified now keeps the path the
core was given, is not declared to the service, and is logged.
Each correction has a test, and the error-code split is pinned by one that walks
the codes rather than trusting a message.
* build(deps): resolve the service protocol crate from its pushed branch
The staging work needed protocol revision 2, which did not exist anywhere yet, so
the last three commits built against a local checkout supplied by an untracked
`[patch]` and deliberately left `Cargo.lock` out. That branch is pushed now, so the
pin names it and the lock records the commit rather than a path on one machine —
this tree builds for anyone again.
Still a branch and not a tag: the protocol has only been exercised by the service's
own tests and by this client compiling against it, never by the two running
together. Moving to v2.6.0 is worth doing once it has.
* build(deps): take the kode-bridge listener-mode fix
Only reaches this repository through the service protocol crate, but the lock is
where the commit is recorded, so it has to move here too.
kode-bridge applied a listener's permission mode as a bind-time option on every
unix except macOS, where the umask filters it — a service asking for 0o666 got
0o644 or 0o600 depending on the host. That is why the service published a control
socket on Linux that no unprivileged client could open.
* build(deps): advance the service protocol pin to the branch head
The lock still named the commit the branch had when it was first pinned, five
commits ago; two of those are the Windows CI fixes. Nothing in them changes what
this repository compiles — the parts they touch are behind the `test` feature,
which this dependency does not enable — but a lock that names a commit the branch
has moved past means the next person to rebuild gets something different from what
was tested here.
Only the commit changes. `cargo update -p clash_verge_service_ipc` also rewrites
twelve unrelated crates from `windows-sys 0.61.2` down to 0.60.2 and 0.52.0, which
has nothing to do with this pin and is not wanted; `cargo check --locked` confirms
the 0.61.2 resolution is still valid, so the downgrade is the resolver's preference
rather than a requirement.
* fix(ci): unblock Windows Rust checks
* feat(service): isolate macOS launch and owner runtime
* ci(windows): run owner security tests
* chore(deps): use kode-bridge v0.4.1 tag
* fix(macos): classify installed service state
* fix(macos): classify marked unavailable service
* fix(macos): gate core startup on service migration
* fix(macos): scope service init to supported platforms
* feat(macos): prompt before migrating installed service
* fix(macos): shorten sidecar IPC path
* build(service): pin helper protocol and artifacts
* fix(core): require confirmed service and sidecar readiness
* fix(macos): harden launch roots and singleton state
* fix(ui): refresh migration state before restart
* fix(macos): linearize service migration lifecycle
* fix(ui): keep service migration state fresh
* feat(service): track authenticated owner sessions
* fix(service): preserve newer owner sessions
* feat(macos): route service proxy changes through helper
* fix(macos): serialize service proxy guard
* fix(macos): close service proxy owner lifecycle
* fix(macos): close remaining proxy stop races
* fix(app): do not cancel controlled service stop
* fix(lifecycle): keep controlled updates cancellation-safe
* fix(macos): stage dev core for service
* test(macos): exercise staged core cleanup
* fix(dev): await controlled macOS exit
* chore: update changelog
* fix(macos): recover sidecar after service install failure
Allow the existing sidecar to continue when service installation leaves the manager in InstallRequired, then settle the session state as SidecarAllowed. Show neutral status text while the migration dialog waits for its final state.
* fix(macos): stage dev service tools before elevation
* fix(macos): localize launch guard prompts
* fix(service): address cross-platform integration issues
* feat: support seamless service migration
* fix: pass Windows service installer path safely
* fix(prebuild): derive service release from Cargo dependency
* fix(dev): preserve service installation state
* fix: resolve clippy warnings in service lifecycle
* feat: show detailed results in hotkey notifications
* fix: Japanese locale appears to have a truncated translation key label
* fix: variable naming
* Update documentation to English
* Remove unnecessary mut
* feat: enhance system proxy notifications with toggle state
* chore: update changelog to include new shortcut notification feature
* fix: remove unnecessary quotes from system proxy toggle messages in localization files
* fix: tun mode toggled hotkey notifications
* fix: correct toggle_tun_mode logic to handle current state and errors
---------
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
* feat(limiter): add Limiter struct with clock interface and tests
* feat(limiter): integrate Limiter into tray and window management for rate limiting
* fix(tray, window_manager): update debounce timing for tray click and window operations
* refactor(limiter): change time representation from u64 to u128 for improved precision
* fix: resolve issue with tray operations after system resume
* Revert "refactor(limiter): change time representation from u64 to u128 for improved precision"
This reverts commit 2198f40f7f.
* fix: update service to 2.1.0 and improve service installation for Unix systems
* fix: set GID environment variable during service installation on Linux
* Revert "fix: set GID environment variable during service installation on Linux"
This reverts commit 373aec579b.