Commit Graph
84 Commits
Author SHA1 Message Date
cb751052a4 feat(tray): add a per-group latency test (#8173)
* 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>
2026-10-09 09:11:07 +08:00
wonfen ed0e1b38f0 feat(proxy): support dragging to swap proxy group header sections 2026-09-10 21:39:46 +08:00
wonfen 807f9a206d chore: trim excessive comments 2026-08-19 07:27:43 +08:00
wonfen c90de1355e refactor: core runstate (#7602)
* 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
2026-07-27 00:44:53 -07:00
wonfen ece3c71f38 feat(proxies): add empty-state troubleshooting guidance 2026-07-26 01:28:49 +08:00
wonfen 304f5bade9 perf(proxy): consolidate proxy data into a single IPC (#7542)
* feat(proxy): build deterministic proxy view

* feat(proxy): expose ordered proxy view command

* feat(proxy): add proxy view client contract

* fix(lint): allow Node test files

* feat(proxy): switch UI to ordered proxy view

* fix(proxy): harden ordered proxy view consumers

* fix(proxy): compare chain duplicates by current id

* fix(proxy): release mihomo guard after fetch

* fix(proxy): preserve semantic selections across refresh

* fix(proxy): block global chain without GLOBAL

* fix(proxy): handle proxy view edge states
2026-07-21 00:11:59 +08:00
oomeow 1847002ebe fix: group tools filter and layout (#7431)
* fix(proxy): group filter

* refactor(proxy): improve layout and structure of proxy render components

* refactor(proxy): remove unnecessary flushSync calls in proxy-group-tools

* feat(proxy): debounce filter input in ProxyGroupTools

* refactor(base-search-box): initialize lastSearchStateRef with current state

* refactor(proxy): remove debug console.log in proxy-groups

* refactor(proxy): improve scrolling behavior in proxy groups

* refactor(proxy): improve scroll position restoration logic

* chore(proxy): reset proxy group tools layout

* style(proxy): adjust margins and spacing in proxy components

* refactor(proxy): remove commented out ProxyGroupTools component
2026-07-11 07:38:32 +00:00
oomeowandTunglies 562828e14d fix: provider proxy delay check (#7456)
* refactor(proxy): support delay testing for provider-based proxies

* style: remove debug console logs

* docs: update changelog

* feat(services): merge provider nodes into records in calcuProxies

---------

Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2026-07-11 05:24:41 +00:00
oomeow dd3ef698f5 feat: sticky groups (#7195)
* feat(proxy): implement sticky virtual list and group tools

- Add new StickyVirtualList component for better performance with sticky headers
- Create ProxyGroupTools component for filtering, sorting and testing proxy groups
- Enhance proxy groups with improved scrolling and group toggle functionality
- Integrate search box with click event handling
- Remove redundant ProxyHead component usage

* style: remove unused import and update TypeScript types

* refactor: convert function component to forwardRef component to fix biome lint error

* feat(proxy): extract proxy chain UI to separate component

* refactor: remove custom throttle implementation and simplify dependencies

* feat(proxy): improve proxy groups UI and scrolling behavior

- Add initialOffset prop to StickyVirtualList for better scroll position restoration
- Refactor ProxyGroups component to separate chain and normal modes
- Implement scroll position persistence for both chain and normal modes
- Add sticky shadow effect for expanded proxy groups
- Remove unnecessary ScrollTopButton component
- Optimize scroll event handling with throttling
- Improve code organization with custom hooks and component separation

* docs(proxy-groups): Add TODO comment for initial render position issue

* feat(proxy): improve scroll position handling and UI display

* refactor(proxy): simplify scroll handling logic and remove unused code

* fix(sticky-virtual-list): remove duplicate styles and add padding

* refactor(proxy): rename scroll offset variables and functions for clarity

* refactor: remove unnecessary refs and simplify onClick handlers

* fix(proxy-groups): handle initial scroll position state

* feat(zed): add biome formatter configuration

* refactor(proxy): remove unused headState import and type annotations

* style(proxy): improve type imports and conditional rendering

* .

* feat(proxy): add ProxyHead component to render in global mode

* chore: ignore .zed directory

* docs: update changelog
2026-07-02 13:22:40 +00:00
Tunglies 55060d422a fix: include url-test groups in proxy selectors 2026-06-29 03:14:03 +08:00
Tunglies e71e701fd0 refactor: migrate query cache to swr 2026-06-28 19:39:15 +08:00
Tunglies 4ee63b6cf2 perf(proxies): optimize virtual list scrolling 2026-06-18 20:53:22 +08:00
Tunglies fc2ff69bdb fix(proxy): stabilize proxy group sticky scroll #6982
Close #6982
2026-05-16 02:48:51 +08:00
wonfen db0fee7894 feat: add sticky scroll for proxy groups 2026-05-03 03:55:50 +08:00
Tunglies 6e0647f7b2 perf: narrow app data subscriptions 2026-04-29 10:18:03 +08:00
Tunglies d203121abe perf(ui): reduce CPU and memory pressure on hot frontend paths
WebSocket hooks (use-connection-data, use-log-data)
- Single-pass merge for connection snapshots; reuse previous row reference
  when upload/download are unchanged so downstream memos can bail out
- Single-allocation cap on closedConnections (no double concat+slice)
- Single-allocation clamp in appendLogs
- Cache dayjs().format result within each 50ms log flush window

Connection table (connection-table.tsx)
- Decouple 5s relative-time tick from columnDefs via shared module-level
  tickStore + memoised RelativeTimeCell so the tick no longer rebuilds
  the table
- RowComponent memo with custom comparator (relies on stable row reference)
- Lift sx objects to module scope
- Stable onShowDetail via ref

App data provider (app-data-provider, app-data-context)
- Split monolithic context into six sub-contexts (Proxies/Rules/ClashConfig/
  System/Uptime/Refreshers); 3s uptime ticks no longer invalidate unrelated
  consumers
- useAppData adapter aggregates sub-contexts; consumer files unchanged
- useStableFn helper stabilises all refresh* callback identities
- Extract useCtx helper to remove repeated null-check boilerplate

Canvas traffic graph (enhanced-canvas-traffic-graph, use-traffic-monitor)
- Lift Y-axis scale (max/min) to drawGraph layer; calculateY allocations
  per frame: ~600 -> 1
- Document visibility gate stops RAF and the setNow interval when the
  window is hidden; appendData unaffected
- Reuse the existing visibilitychange listener instead of adding a second
  one; isDocumentVisibleRef guards animate() inside the frame
- Convert forEach arrow shorthands to block bodies
- Drop unnecessary state-setter deps from a useCallback

Proxy groups (proxy-groups, proxy-render, use-render-list)
- Narrow proxyColItemsMemo dep: headState -> headState.showType
- Per-group fingerprint cache + prevListRef in use-render-list; steady-state
  refetch ticks become O(0) and the list reference stays stable when nothing
  changed
- useStableCallback wraps handleCheckAll / handleLocation
2026-04-19 19:33:23 +08:00
Tunglies 2e505f26ae feat(proxy-groups): integrate useQuery for fetching proxies data 2026-04-15 15:07:04 +08:00
Tunglies d6d15652ca refactor: migrate react-virtuoso to @tanstack/react-virtual 2026-04-03 17:13:13 +08:00
Slinetrac c672a6fef3 refactor: lint (#6511)
* refactor: lint

* chore: remove eslint-plugin/config-prettier
2026-03-15 07:40:11 +00:00
Sline fd98caccd2 revert: use-app-data (#6088)
* Revert "refactor(app-data): split monolithic context into focused SWR hooks (#5576)"

This reverts commit 8e8182f707.

# Conflicts:
#	src/components/home/clash-info-card.tsx
#	src/components/home/clash-mode-card.tsx
#	src/components/home/current-proxy-card.tsx
#	src/components/home/home-profile-card.tsx
#	src/components/proxy/provider-button.tsx
#	src/components/proxy/proxy-chain.tsx
#	src/components/proxy/proxy-groups.tsx
#	src/components/proxy/use-render-list.ts
#	src/components/rule/provider-button.tsx
#	src/components/setting/mods/sysproxy-viewer.tsx
#	src/hooks/use-clash-data.ts
#	src/hooks/use-current-proxy.ts
#	src/hooks/use-shared-swr-poller.ts
#	src/hooks/use-system-proxy-state.ts
#	src/pages/rules.tsx

* docs: Changelog.md
2026-01-16 18:32:31 +08:00
Slinetrac a981be80ef refactor(base): expand barrel exports and standardize imports 2026-01-06 15:02:10 +08:00
Slinetrac 86dcf0bdcf fix: proxy chain #5776
Closes #5776
2025-12-11 16:23:43 +08:00
Tunglies dfa5cff1b7 feat(proxy-groups): filter available groups to show only Selector types in chain mode 2025-12-10 10:04:20 +08:00
Slinetrac 3cf51de850 refactor: reorganize component and hook structure; remove unused modules 2025-12-04 12:51:22 +08:00
Slinetrac 882f1a2a4a Revert "feat: imporve the layout of proxies page (#5647)"
This reverts commit 8c1caf8555.
2025-11-29 20:40:45 +08:00
DengHanjie 8c1caf8555 feat: imporve the layout of proxies page (#5647)
* feat: imporve the layout of proxies page to avoid navigator overlapping items (issue #5556)

* feat(proxy): remove left padding
2025-11-29 11:08:58 +08:00
Sline caca8b2bc4 refactor: debugLog for frontend (#5587) 2025-11-25 10:01:04 +08:00
Sline 8e8182f707 refactor(app-data): split monolithic context into focused SWR hooks (#5576)
* refactor(app-data): split monolithic context into focused SWR hooks

* refactor(swr): unify polling and consolidate proxy/config/provider data flow
2025-11-24 16:18:31 +08:00
Slinerenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>rozan
c8aa72186e chore: i18n (#5276)
* chore: notice i18n

* feat: add script to clean up unused i18n keys

* chore: cleanup i18n keys

* refactor(i18n/proxies): migrate proxies UI to structured locale keys

* chore: i18n for rule module

* chore: i18n for profile module

* chore: i18n for connections module

* chore: i18n for settings module

* chore: i18n for verge settings

* chore: i18n for theme settings

* chore: i18n for theme

* chore(i18n): components.home.*

* chore(i18n): remove unused i18n keys

* chore(i18n): components.profile.*

* chore(i18n): components.connection

* chore(i18n): pages.logs.*

* chore(i18n): pages.*.provider

* chore(i18n): components.settings.externalCors.*

* chore(i18n): components.settings.clash.*

* chore(i18n): components.settings.liteMode.*

* chore(i18n): components.settings.backup.*

* chore(i18n): components.settings.clash.port.*

* chore(i18n): components.settings.misc.*

* chore(i18n): components.settings.update.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): pages.profiles.notices/components.providers.notices

* refactor(notice): unify showNotice usage

* refactor(notice): add typed showNotice shortcuts, centralize defaults, and simplify subscriptions

* refactor: unify showNotice usage

* refactor(notice): unify showNotice API

* refactor(notice): unify showNotice usage

* chore(i18n): components.test.*

* chore(i18n): components.settings.dns.*

* chore(i18n): components.home.clashInfo.*

* chore(i18n): components.home.systemInfo.*

* chore(i18n): components.home.ipInfo/traffic.*

* chore(i18n): navigation.*

* refactor(i18n): remove pages.* namespace and migrate route texts under module-level page keys

* chore(i18n): common.*

* chore(i18n): common.*

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

* fix: improve error handling in patch_profiles_config to prevent requests during profile switching

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config

* chore(i18n): restructure root-level locale keys into namespaces

* chore(i18n): add missing i18n keys

* docs: i18n guide

* chore: adjust i18n

* refactor(i18n): align UI actions and status labels with common keys

* refactor(i18n): unify two-name locale namespaces

* refactor(i18n/components): unify locale keys and update component references

* chore(i18n): add shared and entities namespaces to all locale files

* refactor(i18n): consolidate shared and entity namespaces across features

* chore(deps): update npm dependencies to ^7.3.5 (#5310)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* refactor(i18n): migrate shared editor modes and consolidate entities namespaces

* tmp

* refactor(i18n): flatten locales and move theme/validation strings

* docs: CONTRIBUTING_i18n.md

* refactor(i18n): restructure feedback and profile namespaces for better organization

* refactor(i18n): unify settings locale structure and update references

* refactor(i18n): reorganize locale keys for home, proxies, rules, connections, logs, unlock, and tests

* refactor(i18n/feedback/layout): unify shared toasts & normalize layout namespace

* refactor(i18n): centralize common UI strings in shared

* refactor(i18n): flatten headers and unify locale schema

* refactor(i18n): consolidate duplicate per-feature translations into shared namespace

* refactor(i18n): split locales into per-namespace files

* style: lint

* refactor(i18n): unify unlock UI translations under tests namespace

* feat(i18n): add type-checked translation keys

* style: eslint import order

* feat(i18n): replace ad-hoc loader with rust-i18n backend bundles

* chore(prebuild): remove locale-copy step

* fix(i18n, notice): propagate runtime params and update cleanup script path

* fix(i18n,notice): make locale formatting idempotent and guard early notice translations

* fix(i18n): resolve locale aliases and match OS codes correctly

* fix(unlock): use i18next-compatible double-brace interpolation in failure notice

* fix(i18n): route unlock error notices through translation keys

* fix(i18n): i18n types

* feat(i18n): localize upgrade notice for Clash core viewer

* fix(notice): ensure runtime overrides apply to prefix translations

* chore(i18n): replace literal notices with translation keys

* chore(i18n): types

* chore(i18n): regen typings before formatting to keep keys in sync

* chore(i18n): simply labels

* chore(i18n): adjust translation

* chore: remove eslint-plugin-i18next

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend (#5341)

* chore(i18n): translate settings.json (missed in previous pass) (#5343)

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* fix(i18n-tauri): quote placeholder-leading value in ko.yml to prevent rust_i18n parse panic

* chore(i18n): translate settings.json (forgot to include previously)

---------

Co-authored-by: rozan <34974262+thelojan@users.noreply.github.com>
2025-11-08 19:40:38 +08:00
Tunglies a869dbb441 Revert "refactor: profile switch (#5197)"
This reverts commit c2dcd86722.
2025-10-30 18:11:04 +08:00
Sline c2dcd86722 refactor: profile switch (#5197)
* refactor: proxy refresh

* fix(proxy-store): properly hydrate and filter backend provider snapshots

* fix(proxy-store): add monotonic fetch guard and event bridge cleanup

* fix(proxy-store): tweak fetch sequencing guard to prevent snapshot invalidation from wiping fast responses

* docs: UPDATELOG.md

* fix(proxy-snapshot, proxy-groups): restore last-selected proxy and group info

* fix(proxy): merge static and provider entries in snapshot; fix Virtuoso viewport height

* fix(proxy-groups): restrict reduced-height viewport to chain-mode column

* refactor(profiles): introduce a state machine

* refactor:replace state machine with reducer

* refactor:introduce a profile switch worker

* refactor: hooked up a backend-driven profile switch flow

* refactor(profile-switch): serialize switches with async queue and enrich frontend events

* feat(profiles): centralize profile switching with reducer/driver queue to fix stuck UI on rapid toggles

* chore: translate comments and log messages to English to avoid encoding issues

* refactor: migrate backend queue to SwitchDriver actor

* fix(profile): unify error string types in validation helper

* refactor(profile): make switch driver fully async and handle panics safely

* refactor(cmd): move switch-validation helper into new profile_switch module

* refactor(profile): modularize switch logic into profile_switch.rs

* refactor(profile_switch): modularize switch handler

- Break monolithic switch handler into proper module hierarchy
- Move shared globals, constants, and SwitchScope guard to state.rs
- Isolate queue orchestration and async task spawning in driver.rs
- Consolidate switch pipeline and config patching in workflow.rs
- Extract request pre-checks/YAML validation into validation.rs

* refactor(profile_switch): centralize state management and add cancellation flow

- Introduced SwitchManager in state.rs to unify mutex, sequencing, and SwitchScope handling.
- Added SwitchCancellation and SwitchRequest wrappers to encapsulate cancel tokens and notifications.
- Updated driver to allocate task IDs via SwitchManager, cancel old tokens, and queue next jobs in order.
- Updated workflow to check cancellation and sequence at each phase, replacing global flags with manager APIs.

* feat(profile_switch): integrate explicit state machine for profile switching

- workflow.rs:24 now delegates each switch to SwitchStateMachine, passing an owned SwitchRequest.
  Queue cancellation and state-sequence checks are centralized inside the machine instead of scattered guards.
- workflow.rs:176 replaces the old helper with `SwitchStateMachine::new(manager(), None, profiles).run().await`,
  ensuring manual profile patches follow the same workflow (locking, validation, rollback) as queued switches.
- workflow.rs:180 & 275 expose `validate_profile_yaml` and `restore_previous_profile` for reuse inside the state machine.

- workflow/state_machine.rs:1 introduces a dedicated state machine module.
  It manages global mutex acquisition, request/cancellation state, YAML validation, draft patching,
  `CoreManager::update_config`, failure rollback, and tray/notification side-effects.
  Transitions check for cancellations and stale sequences; completions release guards via `SwitchScope` drop.

* refactor(profile-switch): integrate stage-aware panic handling

- src-tauri/src/cmd/profile_switch/workflow/state_machine.rs:1
  Defines SwitchStage and SwitchPanicInfo as crate-visible, wraps each transition in with_stage(...) with catch_unwind, and propagates CmdResult<bool> to distinguish validation failures from panics while keeping cancellation semantics.

- src-tauri/src/cmd/profile_switch/workflow.rs:25
  Updates run_switch_job to return Result<bool, SwitchPanicInfo>, routing timeout, validation, config, and stage panic cases separately. Reuses SwitchPanicInfo for logging/UI notifications; patch_profiles_config maps state-machine panics into user-facing error strings.

- src-tauri/src/cmd/profile_switch/driver.rs:1
  Adds SwitchJobOutcome to unify workflow results: normal completions carry bool, and panics propagate SwitchPanicInfo. The driver loop now logs panics explicitly and uses AssertUnwindSafe(...).catch_unwind() to guard setup-phase panics.

* refactor(profile-switch): add watchdog, heartbeat, and async timeout guards

- Introduce SwitchHeartbeat for stage tracking and timing; log stage transitions with elapsed durations.
- Add watchdog in driver to cancel stalled switches (5s heartbeat timeout).
- Wrap blocking ops (Config::apply, tray updates, profiles_save_file_safe, etc.) with time::timeout to prevent async stalls.
- Improve logs for stage transitions and watchdog timeouts to clarify cancellation points.

* refactor(profile-switch): async post-switch tasks, early lock release, and spawn_blocking for IO

* feat(profile-switch): track cleanup and coordinate pipeline

- Add explicit cleanup tracking in the driver (`cleanup_profiles` map + `CleanupDone` messages) to know when background post-switch work is still running before starting a new workflow. (driver.rs:29-50)
- Update `handle_enqueue` to detect “cleanup in progress”: same-profile retries are short-circuited; other requests collapse the pending queue, cancelling old tokens so only the latest intent survives. (driver.rs:176-247)
- Rework scheduling helpers: `start_next_job` refuses to start while cleanup is outstanding; discarded requests release cancellation tokens; cleanup completion explicitly restarts the pipeline. (driver.rs:258-442)

* feat(profile-switch): unify post-switch cleanup handling

- workflow.rs (25-427) returns `SwitchWorkflowResult` (success + CleanupHandle) or `SwitchWorkflowError`.
  All failure/timeout paths stash post-switch work into a single CleanupHandle.
  Cleanup helpers (`notify_profile_switch_finished` and `close_connections_after_switch`) run inside that task for proper lifetime handling.

- driver.rs (29-439) propagates CleanupHandle through `SwitchJobOutcome`, spawns a bridge to wait for completion, and blocks `start_next_job` until done.
  Direct driver-side panics now schedule failure cleanup via the shared helper.

* tmp

* Revert "tmp"

This reverts commit e582cf4a65.

* refactor: queue frontend events through async dispatcher

* refactor: queue frontend switch/proxy events and throttle notices

* chore: frontend debug log

* fix: re-enable only ProfileSwitchFinished events - keep others suppressed for crash isolation

- Re-enabled only ProfileSwitchFinished events; RefreshClash, RefreshProxy, and ProfileChanged remain suppressed (they log suppression messages)
- Allows frontend to receive task completion notifications for UI feedback while crash isolation continues
- src-tauri/src/core/handle.rs now only suppresses notify_profile_changed
- Serialized emitter, frontend logging bridge, and other diagnostics unchanged

* refactor: refreshClashData

* refactor(proxy): stabilize proxy switch pipeline and rendering

- Add coalescing buffer in notification.rs to emit only the latest proxies-updated snapshot
- Replace nextTick with queueMicrotask in asyncQueue.ts for same-frame hydration
- Hide auto-generated GLOBAL snapshot and preserve optional metadata in proxy-snapshot.ts
- Introduce stable proxy rendering state in AppDataProvider (proxyTargetProfileId, proxyDisplayProfileId, isProxyRefreshPending)
- Update proxy page to fade content during refresh and overlay status banner instead of showing incomplete snapshot

* refactor(profiles): move manual activating logic to reducer for deterministic queue tracking

* refactor: replace proxy-data event bridge with pure polling and simplify proxy store

- Replaced the proxy-data event bridge with pure polling: AppDataProvider now fetches the initial snapshot and drives refreshes from the polled switchStatus, removing verge://refresh-* listeners (src/providers/app-data-provider.tsx).
- Simplified proxy-store by dropping the proxies-updated listener queue and unused payload/normalizer helpers; relies on SWR/provider fetch path + calcuProxies for live updates (src/stores/proxy-store.ts).
- Trimmed layout-level event wiring to keep only notice/show/hide subscriptions, removing obsolete refresh listeners (src/pages/_layout/useLayoutEvents.ts).

* refactor(proxy): streamline proxies-updated handling and store event flow

- AppDataProvider now treats `proxies-updated` as the fast path: the listener
  calls `applyLiveProxyPayload` immediately and schedules only a single fallback
  `fetchLiveProxies` ~600 ms later (replacing the old 0/250/1000/2000 cascade).
  Expensive provider/rule refreshes run in parallel via `Promise.allSettled`, and
  the multi-stage queue on profile updates completion was removed
  (src/providers/app-data-provider.tsx).

- Rebuilt proxy-store to support the event flow: restored `setLive`, provider
  normalization, and an animation-frame + async queue that applies payloads without
  blocking. Exposed `applyLiveProxyPayload` so providers can push events directly
  into the store (src/stores/proxy-store.ts).

* refactor: switch delay

* refactor(app-data-provider): trigger getProfileSwitchStatus revalidation on profile-switch-finished

- AppDataProvider now listens to `profile-switch-finished` and calls `mutate("getProfileSwitchStatus")` to immediately update state and unlock buttons (src/providers/app-data-provider.tsx).
- Retain existing detailed timing logs for monitoring other stages.
- Frontend success notifications remain instant; background refreshes continue asynchronously.

* fix(profiles): prevent duplicate toast on page remount

* refactor(profile-switch): make active switches preemptible and prevent queue piling

- Add notify mechanism to SwitchCancellation to await cancellation without busy-waiting (state.rs:82)
- Collapse pending queue to a single entry in the driver; cancel in-flight task on newer request (driver.rs:232)
- Update handle_update_core to watch cancel token and 30s timeout; release locks, discard draft, and exit early if canceled (state_machine.rs:301)
- Providers revalidate status immediately on profile-switch-finished events (app-data-provider.tsx:208)

* refactor(core): make core reload phase controllable, reduce 0xcfffffff risk

- CoreManager::apply_config now calls `reload_config_with_retry`, each attempt waits up to 5s, retries 3 times; on failure, returns error with duration logged and triggers core restart if needed (src-tauri/src/core/manager/config.rs:175, 205)
- `reload_config_with_retry` logs attempt info on timeout or error; if error is a Mihomo connection issue, fallback to original restart logic (src-tauri/src/core/manager/config.rs:211)
- `reload_config_once` retains original Mihomo call for retry wrapper usage (src-tauri/src/core/manager/config.rs:247)

* chore(frontend-logs): downgrade routine event logs from info to debug

- Logs like `emit_via_app entering spawn_blocking`, `Async emit…`, `Buffered proxies…` are now debug-level (src-tauri/src/core/notification.rs:155, :265, :309…)
- Genuine warnings/errors (failures/timeouts) remain at warn/error
- Core stage logs remain info to keep backend tracking visible

* refactor(frontend-emit): make emit_via_app fire-and-forget async

- `emit_via_app` now a regular function; spawns with `tokio::spawn` and logs a warn if `emit_to` fails, caller returns immediately (src-tauri/src/core/notification.rs:269)
- Removed `.await` at Async emit and flush_proxies calls; only record dispatch duration and warn on failure (src-tauri/src/core/notification.rs:211, :329)

* refactor(ui): restructure profile switch for event-driven speed + polling stability

- Backend
  - SwitchManager maintains a lightweight event queue: added `event_sequence`, `recent_events`, and `SwitchResultEvent`; provides `push_event` / `events_after` (state.rs)
  - `handle_completion` pushes events on success/failure and keeps `last_result` (driver.rs) for frontend incremental fetch
  - New Tauri command `get_profile_switch_events(after_sequence)` exposes `events_after` (profile_switch/mod.rs → profile.rs → lib.rs)
- Notification system
  - `NotificationSystem::process_event` only logs debug, disables WebView `emit_to`, fixes 0xcfffffff
  - Related emit/buffer functions now safe no-op, removed unused structures and warnings (notification.rs)
- Frontend
  - services/cmds.ts defines `SwitchResultEvent` and `getProfileSwitchEvents`
  - `AppDataProvider` holds `switchEventSeqRef`, polls incremental events every 0.25s (busy) / 1s (idle); each event triggers:
      - immediate `globalMutate("getProfiles")` to refresh current profile
      - background refresh of proxies/providers/rules via `Promise.allSettled` (failures logged, non-blocking)
      - forced `mutateSwitchStatus` to correct state
  - original switchStatus effect calls `handleSwitchResult` as fallback; other toast/activation logic handled in profiles.tsx
- Commands / API cleanup
  - removed `pub use profile_switch::*;` in cmd::mod.rs to avoid conflicts; frontend uses new command polling

* refactor(frontend): optimize profile switch with optimistic updates

* refactor(profile-switch): switch to event-driven flow with Profile Store

- SwitchManager pushes events; frontend polls get_profile_switch_events
- Zustand store handles optimistic profiles; AppDataProvider applies updates and background-fetches
- UI flicker removed

* fix(app-data): re-hook profile store updates during switch hydration

* fix(notification): restore frontend event dispatch and non-blocking emits

* fix(app-data-provider): restore proxy refresh and seed snapshot after refactor

* fix: ensure switch completion events are received and handle proxies-updated

* fix(app-data-provider): dedupe switch results by taskId and fix stale profile state

* fix(profile-switch): ensure patch_profiles_config_by_profile_index waits for real completion and handle join failures in apply_config_with_timeout

* docs: UPDATELOG.md

* chore: add necessary comments

* fix(core): always dispatch async proxy snapshot after RefreshClash event

* fix(proxy-store, provider): handle pending snapshots and proxy profiles

- Added pending snapshot tracking in proxy-store so `lastAppliedFetchId` no longer jumps on seed. Profile adoption is deferred until a qualifying fetch completes. Exposed `clearPendingProfile` for rollback support.
- Cleared pending snapshot state whenever live payloads apply or the store resets, preventing stale optimistic profile IDs after failures.
- In provider integration, subscribed to the pending proxy profile and fed it into target-profile derivation. Cleared it on failed switch results so hydration can advance and UI status remains accurate.

* fix(proxy): re-hook tray refresh events into proxy refresh queue

- Reattached listen("verge://refresh-proxy-config", …) at src/providers/app-data-provider.tsx:402 and registered it for cleanup.
- Added matching window fallback handler at src/providers/app-data-provider.tsx:430 so in-app dispatches share the same refresh path.

* fix(proxy-snapshot/proxy-groups): address review findings on snapshot placeholders

- src/utils/proxy-snapshot.ts:72-95 now derives snapshot group members solely from proxy-groups.proxies, so provider ids under `use` no longer generate placeholder proxy items.
- src/components/proxy/proxy-groups.tsx:665-677 lets the hydration overlay capture pointer events (and shows a wait cursor) so users can’t interact with snapshot-only placeholders before live data is ready.

* fix(profile-switch): preserve queued requests and avoid stale connection teardown

- Keep earlier queued switches intact by dropping the blanket “collapse” call: after removing duplicates for the same profile, new requests are simply appended, leaving other profiles pending (driver.rs:376). Resolves queue-loss scenario.
- Gate connection cleanup on real successes so cancelled/stale runs no longer tear down Mihomo connections; success handler now skips close_connections_after_switch when success == false (workflow.rs:419).

* fix(profile-switch, layout): improve profile validation and restore backend refresh

- Hardened profile validation using `tokio::fs` with a 5s timeout and offloading YAML parsing to `AsyncHandler::spawn_blocking`, preventing slow disks or malformed files from freezing the runtime (src-tauri/src/cmd/profile_switch/validation.rs:9, 71).
- Restored backend-triggered refresh handling by listening for `verge://refresh-clash-config` / `verge://refresh-verge-config` and invoking shared refresh services so SWR caches stay in sync with core events (src/pages/_layout/useLayoutEvents.ts:6, 45, 55).

* feat(profile-switch): handle cancellations for superseded requests

- Added a `cancelled` flag and constructor so superseded requests publish an explicit cancellation instead of a failure (src-tauri/src/cmd/profile_switch/state.rs:249, src-tauri/src/cmd/profile_switch/driver.rs:482)
- Updated the profile switch effect to log cancellations as info, retain the shared `mutate` call, and skip emitting error toasts while still refreshing follow-up work (src/pages/profiles.tsx:554, src/pages/profiles.tsx:581)
- Exposed the new flag on the TypeScript contract to keep downstream consumers type-safe (src/services/cmds.ts:20)

* fix(profiles): wrap logging payload for Tauri frontend_log

* fix(profile-switch): add rollback and error propagation for failed persistence

- Added rollback on apply failure so Mihomo restores to the previous profile
  before exiting the success path early (state_machine.rs:474).
- Reworked persist_profiles_with_timeout to surface timeout/join/save errors,
  convert them into CmdResult failures, and trigger rollback + error propagation
  when persistence fails (state_machine.rs:703).

* fix(profile-switch): prevent mid-finalize reentrancy and lingering tasks

* fix(profile-switch): preserve pending queue and surface discarded switches

* fix(profile-switch): avoid draining Mihomo sockets on failed/cancelled switches

* fix(app-data-provider): restore backend-driven refresh and reattach fallbacks

* fix(profile-switch): queue concurrent updates and add bounded wait/backoff

* fix(proxy): trigger live refresh on app start for proxy snapshot

* refactor(profile-switch): split flow into layers and centralize async cleanup

- Introduced `SwitchDriver` to encapsulate queue and driver logic while keeping the public Tauri command API.
- Added workflow/cleanup helpers for notification dispatch and Mihomo connection draining, re-exported for API consistency.
- Replaced monolithic state machine with `core.rs`, `context.rs`, and `stages.rs`, plus a thin `mod.rs` re-export layer; stage methods are now individually testable.
- Removed legacy `workflow/state_machine.rs` and adjusted visibility on re-exported types/constants to ensure compilation.
2025-10-30 17:29:15 +08:00
Sline 8657cedca0 feat: add configurable hover jump navigator delay (#5178)
* fix: hover options

* feat: add configurable hover jump navigator delay

- Added `hover_jump_navigator_delay` to Verge config defaults, patch flow, and response payload for persistent app-wide settings.
- Made proxy navigator respect configurable delay via `DEFAULT_HOVER_DELAY` and new `hoverDelay` prop.
- Threaded stored delay through proxy list so hover scrolling uses Verge-configured value.
- Added "Hover Jump Navigator Delay" control in Layout settings with clamped numeric input, tooltip, and toggle-aware disabling.
- Localized new labels in English, Simplified Chinese, and Traditional Chinese.
- Extended frontend Verge config type to include delay field for type-safe access.

* docs: UPDATELOG.md
2025-10-23 13:14:01 +08:00
Slinetrac fbe6cefbdb fix: hover jump navigator 2025-10-19 16:00:45 +08:00
Slinetrac ef9ccafe61 refactor: proxy components 2025-10-15 09:00:03 +08:00
Slinetrac 2e3174baa7 feat(proxy-groups, current-proxy-card): auto-refresh delay sorting
- proxy-groups: recalculate active group head and reapply delay sort after tests so list reorders automatically when "按延迟排序" is active.
- current-proxy-card: add delaySortRefresh trigger after auto/manual latency checks to immediately refresh selector and proxy list ordering.
- current-proxy-card: listen for delaySortRefresh to keep displayed delay chips and option ordering aligned with latest measurements.
2025-10-14 20:25:52 +08:00
oomeow 85f4afe2a1 perf: reduce reset scroller position 2025-10-12 15:09:20 +08:00
oomeow 7fc238c27b refactor: invock mihomo api by use tauri-plugin-mihomo (#4926)
* feat: add tauri-plugin-mihomo

* refactor: invock mihomo api by use tauri-plugin-mihomo

* chore: todo

* chore: update

* chore: update

* chore: update

* chore: update

* fix: incorrect delay status and update pretty config

* chore: update

* chore: remove cache

* chore: update

* chore: update

* fix: app freezed when change group proxy

* chore: update

* chore: update

* chore: add rustfmt.toml to tauri-plugin-mihomo

* chore: happy clippy

* refactor: connect mihomo websocket

* chore: update

* chore: update

* fix: parse bigint to number

* chore: update

* Revert "fix: parse bigint to number"

This reverts commit 74c006522e.

* chore: use number instead of bigint

* chore: cleanup

* fix: rule data not refresh when switch profile

* chore: update

* chore: cleanup

* chore: update

* fix: traffic graph data display

* feat: add ipc connection pool

* chore: update

* chore: clippy

* fix: incorrect delay status

* fix: typo

* fix: empty proxies tray menu

* chore: clippy

* chore: import tauri-plugin-mihomo by using git repo

* chore: cleanup

* fix: mihomo api

* fix: incorrect delay status

* chore: update tauri-plugin-mihomo dep

chore: update
2025-10-08 12:32:40 +08:00
Tunglies 1176f8c863 feat: refactor app data provider and context for improved data management and performance 2025-10-04 21:20:31 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
8b3bc18ea8 chore(deps): update dependency eslint-plugin-react-hooks to v6 (#4940)
* chore(deps): update dependency eslint-plugin-react-hooks to v6

* fix: update ESLint configuration to use correct imports and recommended settings

* chore: clean up unused code and improve readability across components

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-10-04 19:42:34 +08:00
Tunglies 8a4f2de887 Revert "Refactor components to remove forwardRef and simplify props handling"
This reverts commit 1cd013fb94.
2025-09-30 18:13:02 +08:00
Tunglies 1cd013fb94 Refactor components to remove forwardRef and simplify props handling
- Updated multiple components to remove the use of forwardRef, simplifying the props structure.
- Adjusted imports and component definitions accordingly.
- Ensured consistent handling of refs and props across various viewer components.
- Improved readability and maintainability of the codebase.
2025-09-30 14:26:40 +08:00
Junkai W. d86bdea127 feat: add Quick navigation bar in the rule mode agent group (#4889) 2025-09-29 11:51:53 +08:00
Junkai W. 909c4028f1 添加链式代理下规则适配 2025-09-22 18:16:17 +08:00
Tunglies e414b49879 Refactor imports across multiple components for consistency and clarity
- Reorganized import statements in various components to ensure consistent ordering and grouping.
- Removed unnecessary imports and added missing ones where applicable.
- Improved readability and maintainability of the codebase by standardizing import styles.
2025-09-19 00:01:04 +08:00
Tunglies 627119bb22 Refactor imports and improve code organization across multiple components and hooks
- Consolidated and reordered imports in various files for better readability and maintainability.
- Removed unused imports and ensured consistent import styles.
- Enhanced the structure of components by grouping related imports together.
- Updated the layout and organization of hooks to streamline functionality.
- Improved the overall code quality by following best practices in import management.
2025-09-18 23:34:38 +08:00
Junkai W. f2073a2f83 Add Func 链式代理 (#4624)
* 添加链式代理gui和语言支持
在Iruntime中添跟新链式代理配置方法
同时添加了cmd

* 修复读取运行时代理链配置文件bug

* t

* 完成链式代理配置构造

* 修复获取链式代理运行时配置的bug

* 完整的链式代理功能
2025-09-15 07:44:54 +08:00
wonfen 0ea875f7f7 fix: unify homepage node selection 2025-09-01 11:30:27 +08:00
Junkai W. bea0dde074 Win 下添加代理节点的系统托盘 (#4562)
* add proxy memu in tray

* 添加win下系统托盘 节点
代理->代理组->nodes
同时添加了对应gui同步

* 添加win 系统托盘显示代理节点
且gui和托盘刷新机制

* rust format

* 添加 win下系统托盘节点延迟

* Squashed commit of the following:

commit 44caaa62c5
Merge: 1916e539 3939741a
Author: Junkai W. <129588175+Be-Forever223@users.noreply.github.com>
Date:   Sat Aug 30 02:37:07 2025 +0800

    Merge branch 'dev' into dev

commit 3939741a06
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Sat Aug 30 02:24:47 2025 +0800

    refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling (#4568)

    * refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling

    * refactor: format code for better readability in DNS configuration

commit f86a1816e0
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Sat Aug 30 02:15:34 2025 +0800

    chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml (#4564)

    * chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml

    * chore(deps): remove libnghttp2-sys dependency and update isahc features in Cargo.toml

    * chore(deps): remove sysinfo and zip from ignoreDeps in renovate.json

commit 9cbd8b4529
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Sat Aug 30 01:30:48 2025 +0800

    feat: add x86 OpenSSL installation step for macOS in workflows

commit 5dea73fc2a
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sat Aug 30 01:21:53 2025 +0800

    chore(deps): update npm dependencies (#4542)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 01af1bea23
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sat Aug 30 01:21:46 2025 +0800

    chore(deps): update rust crate reqwest_dav to 0.2.2 (#4554)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 1227e86134
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Sat Aug 30 01:12:03 2025 +0800

    Remove unnecessary "rustls-tls" feature from reqwest dependency in Cargo.toml

commit c6a6ea48dd
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Fri Aug 29 23:51:09 2025 +0800

    refactor: enhance async initialization and streamline setup process (#4560)

    * feat: Implement DNS management for macOS

    - Added `set_public_dns` and `restore_public_dns` functions in `dns.rs` to manage system DNS settings.
    - Introduced `resolve` module to encapsulate DNS and scheme resolution functionalities.
    - Implemented `resolve_scheme` function in `scheme.rs` to handle deep links and profile imports.
    - Created UI readiness management in `ui.rs` to track and update UI loading states.
    - Developed window management logic in `window.rs` to handle window creation and visibility.
    - Added initial loading overlay script in `window_script.rs` for better user experience during startup.
    - Updated server handling in `server.rs` to integrate new resolve functionalities.
    - Refactored window creation calls in `window_manager.rs` to use the new window management logic.

    * refactor: streamline asynchronous handling in config and resolve setup

    * Revert "refactor: streamline asynchronous handling in config and resolve setup"

    This reverts commit 23d7dc86d5.

    * fix: optimize asynchronous memory handling

    * fix: enhance task logging by adding size check for special cases

    * refactor: enhance async initialization and streamline setup process

    * refactor: optimize async setup by consolidating initialization tasks

    * chore: update changelog for Mihomo(Meta) kernel upgrade to v1.19.13

    * fix: improve startup phase initialization performance

    * refactor: optimize file read/write performance to reduce application wait time

    * refactor: simplify app instance exit logic and adjust system proxy guard initialization

    * refactor: change resolve_setup_async to synchronous execution for improved performance

    * refactor: update resolve_setup_async to accept AppHandle for improved initialization flow

    * refactor: remove unnecessary initialization of portable flag in run function

    * refactor: consolidate async initialization tasks into a single blocking call for improved execution flow

    * refactor: optimize resolve_setup_async by restructuring async tasks for improved concurrency

    * refactor: streamline resolve_setup_async and embed_server for improved async handling

    * refactor: separate synchronous and asynchronous setup functions for improved clarity

    * refactor: simplify async notification handling and remove redundant network manager initialization

    * refactor: enhance async handling in proxy request cache and window creation logic

    * refactor: improve code formatting and readability in ProxyRequestCache

    * refactor: adjust singleton check timeout and optimize trace size conditions

    * refactor: update TRACE_SPECIAL_SIZE to include additional size condition

    * refactor: update kode-bridge dependency to version 0.2.1-rc2

    * refactor: replace RwLock with AtomicBool for UI readiness and implement event-driven monitoring

    * refactor: convert async functions to synchronous for window management

    * Update src-tauri/src/utils/resolve/window.rs

    * fix: handle missing app_handle in create_window function

    * Update src-tauri/src/module/lightweight.rs

* format
2025-08-31 14:20:57 +08:00
Tunglies 475a09bb54 feat: comprehensive oxlint cleanup - remove unused code
🧹 Cleanup Summary:
- Fixed 83 oxlint warnings across 50+ files
- Removed unused imports, variables, and functions
- Maintained all functional code and error handling
- Improved bundle size and code maintainability

📝 Key Changes:
- Cleaned unused React hooks (useState, useEffect, useClashInfo)
- Removed unused Material-UI imports (useTheme, styled components)
- Deleted unused interfaces and type definitions
- Fixed spread operator usage and boolean casting
- Simplified catch parameters where appropriate

🎯 Files Modified:
- React components: home.tsx, settings, profiles, etc.
- Custom hooks: use-*.ts files
- Utility functions and type definitions
- Configuration files

✅ Result: 0 oxlint warnings (from 83 warnings)
🔧 All functionality preserved
📦 Reduced bundle size through dead code elimination
2025-08-22 18:48:56 +08:00
Tunglies 15a1770ee9 feat: migrate mihomo to use kode-bridge IPC on Windows and Unix (#4051)
* Refactor Mihomo API integration and remove crate_mihomo_api

- Removed the `mihomo_api` crate and its dependencies from the project.
- Introduced `IpcManager` for handling IPC communication with Mihomo.
- Implemented IPC methods for managing proxies, connections, and configurations.
- Updated `MihomoManager` to utilize `IpcManager` instead of the removed crate.
- Added platform-specific IPC socket path handling for macOS, Linux, and Windows.
- Cleaned up related tests and configuration files.

* fix: remove duplicate permission entry in desktop capabilities

* refactor: replace MihomoManager with IpcManager and remove Mihomo module

* fix: restore tempfile dependency in dev-dependencies

* fix: update kode-bridge dependency to use git source from the dev branch

* feat: migrate mihomo to use kode-bridge IPC on Windows

This commit implements a comprehensive migration from legacy service IPC to the kode-bridge library for Windows IPC communication. Key changes include:

Replace service_ipc with kode-bridge IpcManager for all mihomo communications
Simplify proxy commands using new caching mechanism with ProxyRequestCache
Add Windows named pipe (\.\pipe\mihomo) and Unix socket IPC endpoint configuration
Update Tauri permissions and dependencies (dashmap, tauri-plugin-notification)
Add IPC logging support and improve error handling
Fix Windows IPC path handling in directory utilities
This migration enables better cross-platform IPC support and improved performance for mihomo proxy core communication.

* doc: add IPC communication with Mihomo kernel, removing Restful API dependency

* fix: standardize logging type naming from IPC to Ipc for consistency

* refactor: clean up and optimize code structure across multiple components and services

- Removed unnecessary comments and whitespace in various files.
- Improved code readability and maintainability by restructuring functions and components.
- Updated localization files for consistency and accuracy.
- Enhanced performance by optimizing hooks and utility functions.
- General code cleanup in settings, pages, and services to adhere to best practices.

* fix: simplify URL formatting in test_proxy_delay method

* fix: update kode-bridge dependency to version 0.1.3 and change source to crates.io

* fix: update macOS target versions in development workflow

* Revert "fix: update macOS target versions in development workflow"

This reverts commit b9831357e4.

* feat: enhance IPC path handling for Unix systems and improve directory safety checks

* feat: add conditional compilation for Unix-specific IPC path handling

* chore: update cagro.lock

* feat: add external controller configuration and UI support

* Refactor proxy and connection management to use IPC-based commands

- Updated `get_proxies` function in `proxy.rs` to call the new IPC command.
- Renamed `get_refresh_proxies` to `get_proxies` in `ipc/general.rs` for consistency.
- Added new IPC commands for managing proxies, connections, and configurations in `cmds.ts`.
- Refactored API calls in various components to use the new IPC commands instead of HTTP requests.
- Improved error handling and response management in the new IPC functions.
- Cleaned up unused API functions in `api.ts` and redirected relevant calls to `cmds.ts`.
- Enhanced connection management features including health checks and updates for proxy providers.

* chore: update dependencies and improve error handling in IPC manager

* fix: downgrade zip dependency from 4.3.0 to 4.2.0

* feat: Implement traffic and memory data monitoring service

- Added `TrafficService` and `TrafficManager` to manage traffic and memory data collection.
- Introduced commands to get traffic and memory data, start and stop the traffic service.
- Integrated IPC calls for traffic and memory data retrieval in the frontend.
- Updated `AppDataProvider` and `EnhancedTrafficStats` components to utilize new data fetching methods.
- Removed WebSocket connections for traffic and memory data, replaced with IPC polling.
- Added logging for better traceability of data fetching and service status.

* refactor: unify external controller handling and improve IPC path resolution

* fix: replace direct IPC path retrieval with guard function for external controller

* fix: convert external controller IPC path to string for proper insertion in config map

* fix: update dependencies and improve IPC response handling

* fix: remove unnecessary unix conditional for ipc path import

* Refactor traffic and memory monitoring to use IPC stream; remove TrafficService and TrafficManager. Introduce new IPC-based data retrieval methods for traffic and memory, including formatted data and system overview. Update frontend components to utilize new APIs for enhanced data display and management.

* chore: bump crate rand version to 0.9.2

* feat: Implement enhanced traffic monitoring system with data compression and sampling

- Introduced `useTrafficMonitorEnhanced` hook for advanced traffic data management.
- Added `TrafficDataSampler` class for handling raw and compressed traffic data.
- Implemented reference counting to manage data collection based on component usage.
- Enhanced data validation with `SystemMonitorValidator` for API responses.
- Created diagnostic tools for monitoring performance and error tracking.
- Updated existing hooks to utilize the new enhanced monitoring features.
- Added utility functions for generating and formatting diagnostic reports.

* feat(ipc): improve URL encoding and error handling for IPC requests

- Add percent-encoding for URL paths to handle special characters properly
- Enhance error handling in update_proxy with proper logging
- Remove excessive debug logging to reduce noise
- Update kode-bridge dependency to v0.1.5
- Fix JSON parsing error handling in PUT requests

Changes include:
- Proper URL encoding for connection IDs, proxy names, and test URLs
- Enhanced error handling with fallback responses in updateProxy
- Comment out verbose debug logs in traffic monitoring and data validation
- Update dependency version for improved IPC functionality

* feat: major improvements in architecture, traffic monitoring, and data validation

* Refactor traffic graph components: Replace EnhancedTrafficGraph with EnhancedCanvasTrafficGraph, improve rendering performance, and enhance visual elements. Remove deprecated code and ensure compatibility with global data management.

* chore: update UPDATELOG.md for v2.4.0 release, refine traffic monitoring system details, and enhance IPC functionality

* chore: update UPDATELOG.md to reflect removal of deprecated MihomoManager and unify IPC control

* refactor: remove global traffic service testing method from cmds.ts

* Update src/components/home/enhanced-canvas-traffic-graph.tsx

* Update src/hooks/use-traffic-monitor-enhanced.ts

* Update src/components/layout/layout-traffic.tsx

* refactor: remove debug state management from LayoutTraffic component

---------
2025-07-24 00:49:55 +08:00