Reserve one toolbar status area through saving, deployment, and collection
refresh. Show counted transient success and persistent actionable failures,
with distinct applied, unchanged-runtime, and deferred-save results.
Keep cached rows when collection reload fails. Require explicit refresh
and fresh selection after a version conflict.
Validation: 774 hostless tests passed with local ad-hoc signing. Format,
lint, two-axis review, and isolated Chinese/English view rendering checked.
Implement #77 using the complete saved versioned rule collection. Normalize domain, IP, and URL hosts; preserve three outcomes and all deciding identities, source memberships, and other target-specific matches without DNS or runtime dependencies.
Invalidate results when the target changes or refresh starts, and reject late asynchronous publication. Add the native address test panel and approved Chinese/English copy.
Validation: 760 hostless tests and 2 fixed-sslocal semantics smoke tests passed; ad-hoc Debug build passed. Changed-file format/lint clean; existing repository warnings remain. Standards and Spec reviews found no issues. Visual acceptance remains manual.
Bound source metadata and conversion reports to a scrolling region so expansion does not compete with the native table for window constraints. Replace the nested split with a fixed source list and flexible table to keep the rules page inside the main window.
Defer native binding writes until after the current view update, merge filter edits into the latest query, and suppress unchanged query and selection publications.
Validated with 744 hostless tests, an unsigned Debug build, and isolated native window checks at 920x738 and 920x580, including real source selection and disclosure clicks.
Normalize source-independent match/action identities while preserving custom UUIDs. Analyze complete valid candidates for same-action coverage and full or partial opposite-action shadowing without dropping saved intent. Keep duplicate and fixed-local conflicts blocking, including mapped IPv6.
Canonicalize ACL entries without changing runtime source selection or proxy-default skeleton. Verify persistence and no-restart reordering through the Controller, collection permutations, and pinned sslocal loopback comparisons.
Validation: 728 hostless tests and 2 pinned-binary smoke tests passed with ad-hoc arm64 signing; format, lint (3 existing warnings, 0 errors), and diff checks passed. No real system-proxy writes or automatic visual checks.
The home server list card redesign in 1c706aa dropped the trailing
manage-servers button; restore it in the HomeCard trailing slot wired
to the existing onManageServers callback.
Preserve catalog hierarchy and order, separate selection from activation, and mark active targets and ancestors. Persist independent home expansion preferences and support explicit locate and keyboard navigation.
Show server names only and surface activation eligibility, skipped members, and failures. Refine row status visibility and use semantic selection colors for outlined activation buttons, including hover, pressed, disabled, contrast, and inactive-window states.
Validation: 695 hostless macOS arm64 tests passed for the tree implementation; 7 focused tests and ad-hoc app compilation passed after the semantic button refinement. Format and lint checks passed. Native UI interactions and theme changes remain unverified.
Group and server rows in the home target tree carried redundant
subtitles (source labels and a per-group server count) duplicating
what the tree structure already shows. Remove them so each item is
a single-line title, bumped callout to body for a stronger title
presence. TargetGroupInfo loses the now-unused sourceLabel field.
Both server trees held collapse state inside the destination
subtree — ServersView relied on OutlineGroup's built-in expansion
state and the home target tree kept a local @State set — so every
destination switch destroyed it. Hoist a shared
CatalogExpansionState to the composition root; the servers sidebar
and the home target tree now read and write one collapse set.
ServersView replaces OutlineGroup with a manual collapse tree
(flattened visible rows, .tag keeps List selection), trading away
OutlineGroup's arrow-key expand/collapse. MainApp extracts
makeRuntimeController to stay within the function-body lint budget.
Hoist "open main window" into its own section at the top; drop
the in-menu "update all subscriptions" and "copy HTTP export
line" entries, which stay available in the main workspace. The
removed copy path retires the menu's clipboard seam and its
error-alert plumbing.
Implements issue #71 with ADR-0020 (supersedes ADR-0019's endpoint-signature
cleanup, startup cleanup, and health-gate cleanup).
- New ShadowsocksX-NG2SystemProxyHelper target: app-bundled root LaunchDaemon
(SMAppService.daemon, on-demand via MachServices) that mechanically applies
a closed typed configuration or executes an unconditional clear. Clients are
accepted by code-signing bundle identifier only; requests run serially on a
shared queue (last-writer-wins).
- SystemProxyConfiguration is now the closed typed apply payload (Codable over
JSON XPC): explicit SOCKS/HTTP/HTTPS enable + endpoints, PAC and
auto-discovery disabled with URL/JS removed, explicit simple-hostname
exclusion, complete ExceptionsList. Unmodeled Proxies keys are preserved and
equivalent values skip the commit.
- Cleanup is a side effect of a persisted system-proxy intent on-to-off
transition only: the switch itself, or the agent-off cascade (persist both
intents off, clear, then stop listeners; failed cascade persistence records
no transition and clears nothing). Launch with the switch off, health-gate
closure, invalid targets, and passive events never clear. Failed clears are
surfaced without deferred retry.
- Endpoint-signature store, ownership migration, and direct Authorization
Services writes are removed; old signature files stay inert. Helper
unavailability or pending approval keeps an enabled intent pending and
surfaces the Login Items approval path (HomeView button + menu label).
Migrate SilentLaunchStore from its versioned JSON file to the app's
standard defaults domain (key `silentLaunch`), the platform's canonical
home for GUI-scalar preferences, so the GUI preference category stops
growing one file per item. Reads stay safe-side strict: a missing key
or a value not bridgeable to Bool loads as not silent (plain
bool(forKey:) would coerce "yes" to true). The file-form
persistence-failure naming path is dropped — UserDefaults.set reports
no failure — so SilentLaunchController keeps its persist-then-flip seam
while errorMessage and its SettingsView presentation go away.
The legacy file never shipped in any release; migrateLegacyFileIfPresent
runs in the production composition root only, preserves the value on
developer machines, and deletes the file (no-op when absent). The test
composition uses a dedicated suite cleared at assembly; unit tests use
per-test suites with removePersistentDomain teardown, never the
standard domain.
ADR-0017 carries a dated amendment (Status plus the affected
consequence line); the settings.json separation rationale stands
unchanged.
- switch to regular while the workspace window is open (Dock icon,
app switcher, default menu bar) and back to accessory when it closes;
driven by NSWindow didBecomeKey/willClose with an anchor-view identity
filter (scenePhase follows the app rather than windows on macOS, so it
fires no event on close)
- add silent launch preference (settings-general, off by default,
persisted in its own silent-launch.json since settings.json is rewritten
whole from the controller's in-memory snapshot): suppressed launches
keep the menu-bar baseline and proxy runtime restoration is unaffected
- docs: CONTEXT.md adaptive app form / silent launch terms and invariants;
ADR 0017 superseding 0016 (scene-based model and reopening carry over)
A research doc was treated as an adopted in-flight design and a design
round was built on its proposal until corrected: nothing in the repo
documents said docs/research/ holds capability studies rather than
project state, so the directory's semantic status had to be inferred.
State it beside the other domain-truth pointers — research reports
record what was investigated at writing time, current state and adopted
designs live in code, CONTEXT.md and docs/adr/, and a report cited as
implementation basis must be verified in code first.
Every catalog commit re-expanded the active target and ran the full
deploy cycle unconditionally: the status face flashed back to starting,
the launch health gate re-probed both local inbounds, and system proxy
converged again — even when the derived contract was byte-identical to
what was already deployed. Remark edits went further: remarks were part
of SslocalServerDocument, so renaming the active server rewrote the
contract and signalled sslocal for a change with no runtime effect.
The reload predicate is now contract byte equality, shared by both
layers (CONTEXT.md: a committed edit that leaves the runtime
configuration file's bytes unchanged triggers no runtime convergence
work). remarks leave the contract — upstream treats them as optional
metadata and the GUI resolves display names from the catalog — so
remark-only edits and no-op re-saves derive byte-identical documents.
The catalogDidCommit deploy path skips write, signal, health gate and
system proxy converge when ProxyRuntimePlan.actions returns empty
(registered agent, live wrapper, on-disk contract equals the derived
document); disk drift or a dead wrapper keeps the self-healing deploy.
Catalog revalidation (candidate skips, target invalidation) still runs
on every commit, and explicit intents (activation, switches, settings,
launch resync) are untouched.
The remark-edit runtime test — which encoded the old behavior as the
example of a value change — is replaced by four catalog-convergence
cases: remark-only edit and outside-subtree edit stay untouched, group
member and active server value edits rewrite and signal.
Also repairs the smoke suite, which failed on HEAD before this change:
its ACL documents used a sslocal-active.acl path while
RuntimeFileStore.write validates against acl-active.ini, so every
ACL-bearing case threw before spawning the wrapper. Smoke ACL paths now
bind to the store-validated location, and the suite passes 11/11 against
real sslocal, confirming upstream accepts the contract without remarks.
System proxy convergence runs after every successful deploy (activation,
mode change, settings saves, health convergence, launch resync), and each
apply locked SCPreferences and rewrote every owned service even when the
projected dictionaries were semantically equal to what macOS already
applies — so the admin authorization prompt fired on every redeploy while
intent was on, including server switches and GUI relaunches that cannot
change the system proxy dictionary at all.
New SystemProxyPlanner (Domain, pure) decides per service by comparing the
desired Proxies dictionary with the live value: equal services are not
written, and an all-equal apply returns .unchanged without locking or
committing, so no authorization is requested. A foreign change that
coincidentally equals the desired values (or an unowned service already at
them) is adopted into ownership with the recorded original preserved
instead of reported as a conflict. Owned services keep their recorded
original verbatim — nil stays nil — fixing a latent restore bug where a
second real change on a service originally taken over with no proxies
dictionary recorded 2.0's own previous values as the restore point. Real
value changes still write and authorize once.
SystemProxyControlling.apply returns written/unchanged; the gate emits a
systemProxyUnchanged event line (diagnostic log view and redacted report)
so it is observable why no prompt appeared. SystemProxyError moves to
Domain with the planner. CONTEXT.md invariant extended; decision table and
test points in issue #70. Closes#70.