Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
261 changes: 235 additions & 26 deletions .agents/architecture.md

Large diffs are not rendered by default.

69 changes: 67 additions & 2 deletions .agents/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ make core-macos ARCH=arm64
make core-android TARGET_PLATFORM=android-arm64
```

Core builds use setup's input fingerprint cache. Pass `FORCE=1` to bypass it,
for example `make core-macos ARCH=arm64 FORCE=1`.

The Makefile wraps `plugins/setup/buildkit/run_build_tool.sh`; prefer the `make` entry points unless debugging the build tool itself.

## Flutter Development

The project is pinned with FVM.
The project follows FVM's `stable` channel locally. Release CI pins an exact
Flutter version separately; see `.agents/project.md`.

```bash
fvm flutter pub get
Expand Down Expand Up @@ -83,6 +87,7 @@ Tests use `package:test/test.dart` for pure Dart logic and `flutter_test` for pr
```bash
flutter test test/models/
flutter test test/core/
flutter test test/core/desktop/
flutter test test/providers/
flutter test test/common/
flutter test test/database/
Expand All @@ -93,9 +98,62 @@ flutter test plugins/proxy/test/proxy_test.dart

Root `flutter test` only discovers the root package's `test/` directory by default. Include bundled plugin Dart tests by passing paths explicitly, or run `flutter test` from that plugin package directory. Native plugin tests under platform folders are not run by `flutter test`.

For the current Core/service architecture, useful focused checks are:

```bash
flutter test test/core/desktop/
flutter test test/core/service_test.dart
flutter test test/core/protocol_contract_test.dart
flutter test test/manager/core_manager_test.dart
flutter test test/providers/action_test.dart test/providers/system_action_test.dart
flutter test test/widgets/core_status_button_test.dart
```

What those suites own:

- `test/core/desktop/`: replaceable IPC transport, RPC request correlation/failure, direct/Helper process leases, and
latest-intent desktop lifecycle convergence.
- `test/core/service_test.dart`: `CoreService` composition and terminal close behavior.
- `test/core/protocol_contract_test.dart`: shared Dart/Go method and event-envelope compatibility, including event batches.
- `test/providers/action_test.dart`: Core start/restart orchestration and overlapping restart requests.
- `test/providers/system_action_test.dart`: ordered, idempotent exit cleanup and watchdog behavior.
- `test/widgets/core_status_button_test.dart`: 600-millisecond connecting presentation hold, immediate failure display,
long-running connecting state, and disconnected restart.

## Native Component Verification

The CI Go-wrapper checks can be reproduced without CGO:

```bash
cd core
CGO_ENABLED=0 go test .
CGO_ENABLED=0 go vet .
```

The Windows Helper's loopback/session protocol tests are host-independent by default. Windows CI additionally enables its
service implementation:

```bash
cargo fmt --manifest-path services/helper/Cargo.toml -- --check
cargo test --manifest-path services/helper/Cargo.toml
cargo test --manifest-path services/helper/Cargo.toml --features windows-service
```

The last command requires Windows for meaningful service coverage. Native Android lifecycle edits should at minimum
compile the modules they touch; use JDK 17 in this checkout:

```bash
cd android
JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home ./gradlew :service:compileDebugKotlin
JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home ./gradlew :app:compileDebugKotlin
```

Always-on VPN entry, system VPN revoke, actual permission UI, and rapid device start/stop still require Android device or
emulator validation; Kotlin compilation cannot prove those system callbacks.

## Verify

CI runs these in order:
The tag-triggered release workflow runs these root-package checks in order:

```bash
flutter pub get
Expand All @@ -104,3 +162,10 @@ flutter test --reporter expanded
```

Run `flutter analyze` locally before committing when practical.

The workflow runs only for `v*` tag pushes; pull requests do not trigger it.
Root analysis excludes `plugins/**`, and root tests do not discover nested
plugin packages, so CI also validates local Flutter packages, the setup build
tool, the Go wrapper, and Rust components from their own package directories. A
separate Windows runner compiles and tests the helper's `windows-service`
feature before release builds can start.
6 changes: 4 additions & 2 deletions .agents/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ FlClash is a multi-platform proxy client based on ClashMeta (mihomo), built with

## Version Notes

- `.fvmrc` pins Flutter 3.35.7 for local development.
- CI uses Flutter 3.41.9. These may diverge; trust CI as the source of truth for release builds.
- `.fvmrc` follows the FVM `stable` channel for local development; it does not
pin an immutable Flutter version.
- Release CI pins Flutter 3.44.4. Local `stable` may diverge, so trust the CI
version as the source of truth for release builds.
- Dart SDK constraint: `>=3.8.0 <4.0.0`.

## Build Dependencies
Expand Down
33 changes: 33 additions & 0 deletions .agents/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,37 @@ Generated directories are excluded from analysis:
- `lib/**/generated/**`
- `plugins/**`

## Core API Safety

- Do not expose direct filesystem deletion APIs through Core or helper IPC; use
a scope-specific cleanup API instead.
- Keep the shared `CoreMethodCall`/`CoreMethodResponse` JSON envelope structurally identical across Dart, Go, JNI, and
desktop IPC. Do not double-encode `arguments`, `result`, or event batches.
- Keep high-volume log/request events separate from state-bearing events in `core/message.go`; bulk backpressure must not
evict delay, loaded-provider, or geo-update state.

## Lifecycle Rules

- Desktop process ownership belongs to `DesktopCoreLifecycle`; do not start/kill `FlClashCore` from providers, widgets,
managers, or ad hoc exit callbacks. Acquire and release it through a `CoreProcessLease`.
- `CoreController.close()` and platform `close()` implementations are terminal and idempotent. Application shutdown must
stay centralized in `SystemAction`/`SystemExitCoordinator`.
- Android start/stop MethodChannel calls are optimistic UI commands. Keep latest-wins arbitration in native
`ServiceState`; do not add a Flutter completion callback that creates a second lifecycle owner.
- Android service callbacks are not automatically user intent. Route explicit Quick Settings, Always-on VPN, and revoke
actions through `ServiceState` and keep `ServiceController` as the sole binding/run-time owner.
- Every `BroadcastReceiver.goAsync()` path must finish its `PendingResult` exactly once. A watchdog may release the
broadcast lease, but must not cancel, reverse, or otherwise redefine the service operation.
- Presentation smoothing such as `CoreStatusButton`'s connecting hold must remain local display state. It must not delay or
overwrite `coreStatusProvider`, and a real failure must bypass/cancel the hold immediately.

## Testing Rules

The `core/` directory is excluded from automated coverage accounting. Do not add coverage instrumentation or coverage
collection for code under `core/`. CI still runs `CGO_ENABLED=0 go test .` and `go vet .` to compile/check the Go wrapper;
verify cross-language protocol behavior through shared Dart contract tests under `test/core/` and native platform build
checks.

Use `CoreController.test(mock)` to inject a mocked `CoreHandlerInterface`. Call `CoreController.resetInstance()` in `tearDown` to clean up the singleton between tests.

Register fallback values for freezed params used with `any()` matchers.
Expand All @@ -35,10 +64,14 @@ notifier.update((state) => newValue);

When testing freezed models with nested objects, always round-trip through `jsonEncode` and `jsonDecode`. Direct `fromJson(toJson())` fails for nested freezed types because `toJson()` stores child objects directly instead of maps.

For async widgets, put visual cleanup in `finally` when the action may throw. Focused widget tests should cover success,
failure, disposal, and any timer boundary that changes visible state.

## Generated Code

Do not manually edit generated files under:

- `lib/l10n/l10n.dart`
- `lib/models/generated/`
- `lib/providers/generated/`
- `lib/database/generated/`
Expand Down
5 changes: 3 additions & 2 deletions .agents/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ Repo-scoped Codex skills live under `.agents/skills/*/SKILL.md`. Codex can disco

- `localization`: hardcoded UI text scans, ARB updates, locale generation, and localization verification.
- `provider-tests`: Riverpod provider, notifier, and state-management tests.
- `ui-work`: Flutter UI, widgets, Material You styling, navigation surfaces, and user-facing interactions.
- `core-platform`: core integration, platform managers, Go core communication, desktop/mobile behavior, and Windows helper flow.
- `ui-work`: Flutter UI, widgets, Material You styling, navigation surfaces, async feedback, and user-facing interactions.
- `core-platform`: Core lifecycle/process ownership, Android services, Go event delivery, desktop IPC, platform managers,
VPN/TUN, and Windows Helper flow.

## Authoring Notes

Expand Down
58 changes: 45 additions & 13 deletions .agents/skills/core-platform/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,65 @@
---
name: core-platform
description: Use when changing FlClash core integration, platform managers, Go core communication, desktop/mobile platform behavior, or Windows helper flow.
description: Use when changing FlClash Core integration, lifecycle/process ownership, Go event delivery, Android services, desktop IPC, platform managers, VPN/TUN, or Windows Helper flow.
---

# Core And Platform

## When To Use

Use this for changes touching `lib/core/`, `lib/manager/`, `core/`, `services/helper/`, build hooks, system proxy, tray, VPN, TUN, or platform-specific desktop/mobile behavior.
Use this for changes touching `lib/core/`, `lib/manager/`, `core/`, `services/helper/`, Android app/service modules, build
hooks, system proxy, tray, VPN, TUN, or platform-specific desktop/mobile behavior.

## Workflow

1. Identify which boundary owns the behavior:
- Android lib mode: `lib/core/lib.dart`.
- Desktop process/socket mode: `lib/core/service.dart`.
- Shared facade: `lib/core/controller.dart` and `lib/core/interface.dart`.
- Platform lifecycle: `lib/manager/`.
2. Route feature code through `CoreController` and `CoreHandlerInterface`; avoid direct calls to platform implementations outside their boundary.
3. Keep desktop and mobile paths explicit.
4. For action-layer behavior, inspect `lib/providers/action.dart` and relevant generated providers.
5. Add or update shared Dart tests for logic that can be isolated.
6. Manually verify native behavior when automated coverage is not practical.
1. Identify the authoritative owner before changing behavior:
- Shared facade/protocol: `lib/core/controller.dart`, `lib/core/interface.dart`, and `lib/core/method.dart`.
- Android Core connection: `lib/core/lib.dart`, `lib/plugins/service.dart`, and Android `ServicePlugin`.
- Android start/stop intent: `ServiceState`; binding/process-time bookkeeping: `ServiceController`.
- Desktop composition: `lib/core/service.dart`; lifecycle/process ownership: `lib/core/desktop/lifecycle.dart`.
- Desktop IPC/RPC: `lib/core/desktop/transport.dart` and `lib/core/desktop/rpc_client.dart`.
- Desktop launch ownership: `lib/core/desktop/launcher.dart`; Windows Helper HTTP contract:
`lib/core/desktop/helper_client.dart` and `services/helper/`.
- Flutter orchestration: `lib/providers/actions/core.dart` and `system.dart`; UI/event observation: `lib/manager/`.
2. Trace every entry path into that owner, including UI/provider calls, Quick Settings, notification actions, Always-on VPN,
revoke callbacks, application exit, and crash/disconnect recovery. Lifecycle callbacks are not implicit user intent.
3. Preserve latest-intent semantics:
- Desktop revisions converge to running/restarted/stopped/closed and report applied/coalesced/superseded outcomes.
- Android Flutter calls stay optimistic; `ServiceState` identity-checks the latest native `RunRequest`.
4. Route feature calls through `CoreController` and `CoreHandlerInterface`. Do not bypass desktop process leases or create a
second Android service binding owner.
5. Keep JSON envelopes and event shapes identical across Dart, Go, JNI, and desktop IPC. If event traffic changes, preserve
the separate priority and bulk queues in `core/message.go`.
6. Keep shutdown single-owned and terminal. `SystemExitCoordinator` sequences resource cleanup, window close, Core close,
and process exit; widget/manager disposal must not race it.
7. Add or update focused tests at the narrowest layer, then run the matching commands from `.agents/commands.md`:
- Desktop lifecycle/transport/RPC: `test/core/desktop/` plus `test/core/service_test.dart`.
- Cross-language envelopes/events: `test/core/protocol_contract_test.dart` and `CGO_ENABLED=0 go test .`.
- Provider/exit convergence: `test/providers/action_test.dart` and `test/providers/system_action_test.dart`.
- Android Kotlin: compile each touched Gradle module with JDK 17.
- Windows Helper: Cargo format/tests; run the `windows-service` feature on Windows.
8. Explicitly state host gaps. Always-on VPN, VPN permission, system revoke, named-pipe peer identity, and Windows Service
Control Manager behavior need their real platform even when portable tests pass.

## Reference Files

Read `.agents/architecture.md` for the current core modes, manager stack, build hooks, local plugins, and Windows helper notes.

## Pitfalls

- Debug Windows helper auth differs from release token verification.
- Keep the Windows Helper protocol and Core SHA256 validation identical across
Flutter build modes; the Helper owns executable integrity checks.
- Protocol version 5 uses a 32-character lowercase-hex session ID. `/start` must return the submitted session and PID;
`/stop` must never terminate a different session; Dart must verify the connected named-pipe peer PID.
- A desktop process lease with unconfirmed exit must remain owned until cleanup succeeds. Do not discard it and start a
replacement Core.
- `CoreController.close()` is terminal. Do not call it from a reusable manager lifecycle or recover by starting it again.
- `ServiceBroadcastReceiver.goAsync()` must finish once even on timeout; its watchdog releases the broadcast only and must
not become a service timeout.
- Do not interpret service creation/destruction as start/stop intent. Always-on startup is explicit through
`VPN_START_REQUESTED`; revoke is explicit through `VPN_REVOKED`.
- Keep log/request floods from evicting state-bearing Core events. Each queue may evict only its own oldest item.
- Do not expose direct filesystem deletion APIs through Core or helper IPC; use
a scope-specific cleanup API instead.
- `plugins/setup/` is a build harness, not a Dart API plugin.
- Build hooks can trigger Go or Rust compilation indirectly through Flutter platform builds.
19 changes: 17 additions & 2 deletions .agents/skills/ui-work/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: ui-work
description: Use when changing FlClash Flutter UI, widgets, screens, Material You styling, navigation surfaces, or user-facing interactions.
description: Use when changing FlClash Flutter UI, widgets, screens, Material You styling, navigation surfaces, async feedback, or user-facing interactions.
---

# UI Work
Expand All @@ -18,7 +18,12 @@ Use this for user-facing Flutter UI changes in `lib/`, including widgets, screen
5. Prefer `const` constructors and final locals.
6. Localize user-facing text through ARB; use `localization` when text changes are non-trivial.
7. Add focused widget tests when behavior changes, especially for rendering states, taps, scrolling, and empty/error states.
8. Run targeted verification:
8. For asynchronous controls, define separately:
- authoritative provider/domain state;
- display-only state such as a minimum progress duration;
- tap policy while work or display holds are active;
- failure/disposal cleanup, normally in `finally` for animations and timers.
9. Run targeted verification:

```bash
flutter analyze
Expand All @@ -30,3 +35,13 @@ Use this for user-facing Flutter UI changes in `lib/`, including widgets, screen
- Do not introduce a new visual system for one screen.
- Do not manually edit generated localization or provider files.
- Avoid broad layout rewrites unless the requested change requires them.
- Do not mutate provider/domain state merely to smooth a transition. Keep presentation holds local and let real errors
bypass them immediately.
- Do not leave loading animations active when callbacks throw. Test the exception path, not only the successful tap.

## Current Interaction Examples

- `CoreStatusButton` watches `coreStatusProvider` but keeps its 600-millisecond connecting hold locally. Taps are ignored
during the hold or genuine connecting state; disconnected cancels the hold immediately.
- Proxy delay testing writes `0` while pending, the measured delay on success, and `-1` on failure. `DelayTestButton` resets
its animation in `finally`.
28 changes: 28 additions & 0 deletions .github/scripts/generate_release_notes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

set -euo pipefail

previous_tag="${1:-}"
output="${2:-release.md}"

: > "$output"

append_range() {
local range="$1"

git log --no-merges --pretty=format:'%B' "$range" |
awk '!/Update changelog/ && NF {print "- " $0 "\n"}' >> "$output"
}

current_tag=""
while IFS= read -r next_tag; do
if [[ -n "$current_tag" ]]; then
[[ "$current_tag" == "$previous_tag" ]] && break
append_range "$next_tag..$current_tag"
fi
current_tag="$next_tag"
done < <(git tag --merged HEAD --sort=-creatordate)

if [[ -n "$current_tag" && "$current_tag" != "$previous_tag" ]]; then
append_range "$current_tag"
fi
66 changes: 66 additions & 0 deletions .github/scripts/generate_release_notes_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash

set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
generator="$script_dir/generate_release_notes.sh"
temp_dir="$(mktemp -d)"
trap 'rm -rf "$temp_dir"' EXIT

repo="$temp_dir/repo"
git init --quiet --initial-branch=main "$repo"
git -C "$repo" config user.email "release-notes-test@example.com"
git -C "$repo" config user.name "Release notes test"

commit() {
local message="$1"
local date="$2"

GIT_AUTHOR_DATE="$date" GIT_COMMITTER_DATE="$date" \
git -C "$repo" commit --allow-empty --quiet --message "$message"
}

commit "Initial release" "2025-01-01T00:00:00Z"
git -C "$repo" tag v1.0.0

commit "First release change" "2025-01-02T00:00:00Z"
commit "Update changelog" "2025-01-03T00:00:00Z"
git -C "$repo" tag v1.1.0

git -C "$repo" branch feature
git -C "$repo" checkout --quiet feature
commit $'Feature branch change\n\nFeature detail' "2025-01-04T00:00:00Z"
git -C "$repo" checkout --quiet main
GIT_AUTHOR_DATE="2025-01-05T00:00:00Z" \
GIT_COMMITTER_DATE="2025-01-05T00:00:00Z" \
git -C "$repo" merge --no-ff --quiet --message "Merge feature" feature
git -C "$repo" tag v1.2.0

expected="$temp_dir/expected.md"
actual="$temp_dir/actual.md"
printf '%s\n' \
"- Feature branch change" \
"" \
"- Feature detail" \
"" \
"- First release change" \
"" > "$expected"

(
cd "$repo"
bash "$generator" v1.0.0 "$actual"
)
diff -u "$expected" "$actual"

printf '%s\n' "stale content" > "$actual"
(
cd "$repo"
bash "$generator" v1.2.0 "$actual"
)
[[ ! -s "$actual" ]]

(
cd "$repo"
bash "$generator" "" "$actual"
)
grep -q -- "- Initial release" "$actual"
Loading