Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
926a5d2
Add a complete update flow for the app, rules, and Mihomo.
cursoragent Aug 15, 2026
f599acb
Make update checks time out, cancel, and refuse overlap.
cursoragent Aug 15, 2026
cce64b6
Make cloud rule sync retry, stage, and publish atomically.
cursoragent Aug 15, 2026
14fb260
Show a bottom navigation bar on mobile-sized viewports.
cursoragent Aug 15, 2026
1c71d80
Make the About page Basic control open the Basic dashboard.
cursoragent Aug 15, 2026
f5c9d96
Enable vertical scrolling on the Dashboard page.
cursoragent Aug 15, 2026
e477c4b
Keep the status bar sticky at the bottom of the window.
cursoragent Aug 15, 2026
8e2b5de
Make the status-bar internet and IP section refresh on click.
cursoragent Aug 15, 2026
92ebaf5
Show persistent sent and received traffic totals in the status bar.
cursoragent Aug 15, 2026
d5228d1
Install missing helper, Hiddify, and Mihomo before Connect starts.
cursoragent Aug 15, 2026
3e5c44f
Default first launch to Basic mode.
cursoragent Aug 15, 2026
9eb9a56
Fit the connected-state glow to the window edges.
cursoragent Aug 15, 2026
56c119e
Persist and restore the main window size within the display.
cursoragent Aug 15, 2026
f761440
Add responsive coverage for mobile, tablet, and small desktop.
cursoragent Aug 15, 2026
4ff6c5c
Add a functional context menu on text and number inputs.
cursoragent Aug 16, 2026
d46baa8
Update the tray menu to Connect/Disconnect, Pause/Resume, and Quit.
cursoragent Aug 16, 2026
d4de982
Prevent concurrent Connect, Disconnect, Pause, and Resume operations.
cursoragent Aug 16, 2026
cd13aac
Add consistent Lucide icons to every application button.
cursoragent Aug 16, 2026
5040994
Show real connection stages as progress inside the action buttons.
cursoragent Aug 16, 2026
8f827c9
Add a reduced-motion-aware glow to the available Connect button.
cursoragent Aug 16, 2026
84bde0b
Add desktop and mobile README screenshots and a features list.
cursoragent Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ If a required command fails or emits a warning from project code, fix it in the
- The diagnostics **Test flow** button stays disabled until the target field is non-empty.
- The Zustand store is a process singleton. App tests that change `page` must reset store state in `beforeEach`, or the next test stays on Settings and never sees the dashboard heading.
- `getByRole(..., { name: "Install" })` substring-matches **Installing…**. Use `{ name: /^Install$/ }` in Vitest and `{ exact: true }` in Playwright.
- Playwright `getByRole("button", { name: "Connect" })` also matches the status-bar **Internet connected** control. Use `{ name: "Connect", exact: true }`. After click the accessible name becomes the current stage, so keep asserting the same control with `[data-connection-action='connect']`. Stage labels last only a few hundred milliseconds, so record them with a `MutationObserver` instead of sequential `getByRole` name waits. Basic mode has no sidebar **BiFlow** wordmark, so wait for the mode switch instead.
- `scripts/sync-version.mjs` must only sync manifests when it is the process entry point. Importing `readAppVersion` from tests or `build-plan.mjs` must not rewrite `package.json`.
- After installing rustup, the same shell must prepend `$HOME/.cargo/bin` (or `source "$HOME/.cargo/env"`) or `cargo` is still missing. Both `./build.sh` and `./dev.sh` do this before every toolchain check, including clean/non-interactive shells.
- Hiddify/Mihomo Install buttons must use PATH and `~/.local/bin`, not only `~/.local/share/biflow`. Mock UI reads the same locations at Vite startup; Playwright still forces missing deps via `sessionStorage` so e2e can test Install.
Expand Down Expand Up @@ -163,3 +164,9 @@ If a required command fails or emits a warning from project code, fix it in the
- A Windows Connect that dies at Mihomo readiness with `error sending request for url` is not an internal server error. `CoreError::Platform` maps to `errors.internal` in the UI; map a readiness timeout to `ControllerTimeout`. The controller client must use `no_proxy()` or Hiddify's HTTP proxy intercepts `127.0.0.1:19090`. The helper must not `env_clear()` Windows Mihomo down to PATH-only — restore `SYSTEMROOT` (and spawn with `CREATE_NO_WINDOW`), wait briefly for an immediate exit, and ship `wintun.dll` next to `mihomo.exe`.
- A later Windows field log reached `ready: 7` / `rules_loaded: 65734` and then rolled Mihomo back with "process or TUN disappeared". `GET /configs` is still the TUN authority (no adapter enumeration), but `tun.device` on Windows is often `Meta` or empty — treat truthy `tun.enable` as active, retry the post-readiness process/TUN check for 5s, and split the error. Generate Windows YAML like clash-master: `find-process-mode: always`, `ipv6: false`, `auto-redirect: false`, DoH `#VPN`.
- An interrupted `cargo test`/`clippy` can leave `corrupt metadata` in `target/debug/deps/*.rmeta`. Delete only the file rustc names (and its sibling `.rlib`) and rebuild that crate. Do not `cargo clean`.
- A `tokio::sync::watch` subscriber misses intermediate `operation_stage`
values when several `update()` calls run in one worker poll. Yield after
each announced milestone, and collect stages with `tokio::join!` against
`receiver.changed()` so the waiter is armed before the operation is queued.
- `pause_stack` used to treat `Stopped` as already complete without checking the lifecycle lock, so Pause succeeded while Connect was still reserved. Reject any other `busy` kind before the idempotent phase shortcuts.
- Workspace Clippy `map_unwrap_or` rejects `option.map(f).unwrap_or(default)`. Use `map_or(default, f)` at the tray setup site and similar lookups.
23 changes: 12 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ members = [
]

[workspace.package]
version = "3.0.1"
version = "3.3.2"
edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.88"
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
</p>

<p align="center">
<a href="#features">Features</a>
·
<a href="#how-it-works">How it works</a>
·
<a href="#architecture">Architecture</a>
Expand All @@ -21,8 +23,29 @@
<a href="#faq">FAQ</a>
</p>

<p align="center">
<img src="docs/screenshots/desktop.png" alt="BiFlow desktop dashboard while split routing is active" width="800" />
</p>

<p align="center">
<img src="docs/screenshots/mobile.png" alt="BiFlow on a phone-sized window with bottom navigation" width="280" />
</p>

---

## Features

- **Split routing** — Iranian sites, Iranian IP ranges, and private/LAN traffic stay **DIRECT**. Everything else uses the Hiddify connection you already have.
- **Connect, Pause, Resume, Disconnect** — One operation at a time. The active button shows the real stage (Start Hiddify, Start Mihomo, and so on) with an in-button progress fill.
- **Basic and Advanced** — First launch opens Basic. Advanced adds component health, live traffic routes, and extra tools.
- **Direct rules** — Pin hosts to DIRECT or VPN. Refresh the bundled Iran domain and IP lists from the BiFlow cloud snapshot.
- **Diagnostics** — Test whether a host would go DIRECT or through the VPN, then move it. Export or clear the local `debug.log`. Fresh Hiddify start repairs a blank Hiddify window without touching subscriptions.
- **Status bar** — Internet reachability, public IP, approximate country, and lifetime sent/received totals.
- **In-app install** — Connect can install the privileged helper, Hiddify, and the bundled Mihomo build when they are missing.
- **English and Persian** — Built-in UI languages, with a tray menu for Connect/Disconnect, Pause/Resume, and Quit.
- **Linux and Windows** — Debian package, AppImage, portable `.exe`, and NSIS setup. Signed AppImage and NSIS builds can update in-app.
- **Fits the window** — Desktop sidebar, phone-sized bottom navigation, and a resizable window down to 390×640.

## Description

BiFlow is a desktop app for split routing. It keeps Iranian websites, Iranian IP
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@iran-split/desktop",
"version": "3.0.1",
"version": "3.3.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
30 changes: 28 additions & 2 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { APP_VERSION } from "./version";

beforeEach(() => {
resetMockState();
localStorage.removeItem(UI_MODE_STORAGE_KEY);
localStorage.setItem(UI_MODE_STORAGE_KEY, "advanced");
useAppStore.setState({
loading: true,
actionPending: false,
Expand Down Expand Up @@ -63,6 +63,18 @@ describe("App", () => {
expect(screen.getByText("Dariush Vesal")).toBeVisible();
});

it("opens Basic mode on a first launch with no stored preference", async () => {
localStorage.removeItem(UI_MODE_STORAGE_KEY);
render(<App />);
expect(
await screen.findByRole("heading", { name: "Ready when you are" }),
).toBeVisible();
expect(
screen.queryByRole("button", { name: "Direct rules" }),
).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Connect" })).toBeVisible();
});

it("hides advanced chrome in Basic mode", async () => {
render(<App />);
expect(
Expand All @@ -73,7 +85,21 @@ describe("App", () => {
expect(
screen.queryByRole("button", { name: "Direct rules" }),
).not.toBeInTheDocument();
expect(screen.queryByText("Internet connected")).not.toBeInTheDocument();
expect(screen.getByRole("status")).toBeVisible();
expect(screen.getByRole("button", { name: "Connect" })).toBeVisible();
});

it("leaves About for the Basic dashboard when Basic is selected", async () => {
render(<App />);
expect(
await screen.findByRole("heading", { name: "Ready when you are" }),
).toBeVisible();
await userEvent.click(screen.getByRole("button", { name: "About" }));
expect(screen.getByRole("heading", { name: "About" })).toBeVisible();
await userEvent.click(screen.getByRole("radio", { name: "Basic" }));
expect(
screen.queryByRole("heading", { name: "About" }),
).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Connect" })).toBeVisible();
});

Expand Down
Loading
Loading