diff --git a/AGENTS.md b/AGENTS.md index 2837e53..8469668 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,12 +29,18 @@ Domain docs (reference, do not paste into PRs): - [`crates/vnidrop/CORE_FLOW.md`](crates/vnidrop/CORE_FLOW.md) - [`crates/vnidrop/tests/README.md`](crates/vnidrop/tests/README.md) +- **Saved Devices platform UI:** read + [`DEVICE-HISTORY-UI-HANDOFF.md`](DEVICE-HISTORY-UI-HANDOFF.md) before work on + `feat/device-history-kmp` or `feat/device-history-apple`; it defines branch + ownership, PR bases, product behavior, and completion gates. --- ## Absolute rules -1. Prefer PRs into `master`. Do not merge to `master` locally unless the user asks. +1. Prefer PRs into `master`. The Saved Devices platform branches are the + documented exception: their PR base is `feat/device-history`. Do not merge to + `master` locally unless the user asks. 2. Do not `git push`, force-push, or open a PR unless the user asks. 3. If `commit.gpgsign` is enabled, create **signed** commits only. If signing fails (empty `ssh-add -l`), stop and tell the user to unlock the key. Never switch to diff --git a/DESIGN-DEVICE-HISTORY.md b/DESIGN-DEVICE-HISTORY.md index 71b674d..719fd73 100644 --- a/DESIGN-DEVICE-HISTORY.md +++ b/DESIGN-DEVICE-HISTORY.md @@ -1,11 +1,12 @@ # Design — Saved devices and targeted transfers -Status: **production Rust core capability; product UI graduation is deferred**. +Status: **production Rust core capability; platform UI implementation is in progress**. The unreleased contact/held-offer/polling prototype has been removed. The implementation on this branch is the versioned saved-device, device-relationship, and targeted-transfer core described below. Its wire protocol and public core -surface are production contracts; KMP and Apple product UI remain deferred. +surface are production contracts. Platform UI work is coordinated in +[`DEVICE-HISTORY-UI-HANDOFF.md`](DEVICE-HISTORY-UI-HANDOFF.md). The feature lets two VniDrop installations remember one another after a successful transfer, with explicit consent on both devices. A saved device can @@ -13,8 +14,8 @@ then request a new transfer without another invitation, QR scan, or NFC tap. The receiver must still approve every transfer. The Rust core, protocol, persistence, credential-storage integration, and -platform contracts are the first delivery scope. Product UI is intentionally -deferred to a separate design and implementation session. +platform contracts are complete. KMP and Apple product UI ship from separate +branches into `feat/device-history` before the feature targets `master`. --- diff --git a/DEVICE-HISTORY-UI-HANDOFF.md b/DEVICE-HISTORY-UI-HANDOFF.md new file mode 100644 index 0000000..755000a --- /dev/null +++ b/DEVICE-HISTORY-UI-HANDOFF.md @@ -0,0 +1,118 @@ +# Saved Devices UI handoff + +This document coordinates the platform UI work built on the production Saved +Devices and Targeted Transfer core. + +## Branch topology + +| Branch | Ownership | Pull-request base | +|---|---|---| +| `feat/device-history` | Shared core contract and integration base | `master` only when the complete feature is ready | +| `feat/device-history-kmp` | Android, Windows, and Linux Compose UI | `feat/device-history` | +| `feat/device-history-apple` | Native iOS and macOS SwiftUI | `feat/device-history` | + +Create both platform branches from the same `feat/device-history` commit. Keep +platform work on its matching branch. Open every platform PR against +`feat/device-history`, never `master` or the sibling platform branch. When the +base advances, merge or rebase `feat/device-history` into the platform branch; +do not merge one platform branch into the other. + +## Product contract + +- Saved Device is a top-level product feature, not an experimental setting. +- A populated Saved Devices screen has a title-only header. Explanatory copy + belongs in the first-use empty state or next to the control that needs it. +- The main screen lists saved devices and outstanding consent requests. It does + not expose the global Targeted Transfer history. +- Selecting a saved device opens a platform-native details surface: bottom + sheet on compact mobile layouts and a native inspector, sheet, or dialog on + wider layouts. That surface owns Send, label/forget/block actions, and the + device's Targeted Transfers with related lifecycle activity, status, + progress, and available actions. +- Display `localLabel` when present, otherwise the authenticated + `remoteDisplayName`. Keep the endpoint ID secondary and diagnostic. +- Use each platform's native device iconography and interaction conventions. + Equivalent behavior may use separate Apple and Compose implementations. +- Label changes are transactional from the UI's perspective: preserve the + draft and editor on failure, prevent conflicting dismissal/edit actions while + saving, and close only after success. +- Invitation Transfer and Targeted Transfer source composition have file, + folder, editable-name, replacement, and cleanup parity. Keep the domains + distinct after creation. +- Every Targeted Transfer still needs receiver approval. Saving a device never + grants automatic receipt. +- UI and platform code manage pickers and destinations; Rust streams payload + bytes. Android folder selection expands SAF trees into file descriptors and + relative names, never a directory descriptor. + +Use the exact domain terms in [`CONTEXT.md`](CONTEXT.md) and the security and +lifecycle invariants in [`crates/vnidrop/CORE_FLOW.md`](crates/vnidrop/CORE_FLOW.md). +The KMP implementation under +`shared/src/commonMain/kotlin/com/vnidrop/app/feature/saveddevices/` is a tested +behavioral reference, not an Apple visual specification. + +## KMP implementation branch + +Start on `feat/device-history-kmp` and follow +[`shared/AGENTS.md`](shared/AGENTS.md) plus +[`.codex/skills/compose-skill/SKILL.md`](.codex/skills/compose-skill/SKILL.md). + +The branch owns: + +- `shared/`, `androidApp/`, and `desktopApp/` Saved Devices presentation work; +- Material Android and native-feeling Windows/Linux presentations; +- per-device details, transfer composition, offers, pairing consent, label + editing, and lifecycle actions; +- common state-machine tests, JVM Compose interaction tests, and platform + adapter tests. + +Before handoff, run `make check-localization`, `make check-shared`, and the +relevant Android build. Inspect the actual Android emulator and desktop window; +record any host that could not be rendered. + +## Apple implementation branch + +Start on `feat/device-history-apple`. Apple remains native SwiftUI; do not add +Apple presentation to `shared/`. + +The branch owns: + +- a top-level Saved Devices destination in the iOS tab bar and macOS sidebar; +- an Apple-native Saved Devices model/coordinator and SwiftUI screen; +- pairing consent and Targeted Offer presentation outside Experimental + Settings; +- per-device details and Targeted Transfer lifecycle actions; +- iOS/macOS picker and receive-destination integration using the existing + platform services; +- Swift model tests, UI contract tests, and simulator-rendered visual checks. + +Use the generated production UniFFI surface in +`apple/VnidropCore/Sources/VnidropCore/Vnidrop.swift`, including +`listSavedDevices`, `listDeviceRelationships`, `listPairingEligibilities`, +`listPendingTargetedOffers`, `createTargetedTransfer`, +`respondToTargetedOffer`, `listTargetedTransfers`, receive/resume/cancel/delete, +label, forget, and block operations. Wrap those calls through the existing +Apple `CoreGateway` / `CoreRepository` boundary rather than invoking generated +bindings from SwiftUI views. + +Use SF Symbols and native iOS/macOS controls even when that duplicates Compose +presentation code. Share behavior and vocabulary across platforms, not widget +implementations. Before handoff, run `make check-localization` and +`make check-apple`, then inspect the affected iOS and macOS states in real +simulator/app hosts. + +## Completion gate + +Each platform PR is ready only when it demonstrates: + +1. mutual consent creates and names a Saved Device correctly; +2. an already-saved pair is not prompted to save again; +3. files and folders can be composed, changed, and sent to one saved device; +4. Targeted Transfers are absent from the main device list and visible in the + selected device's details surface; +5. receive, resume, cancel, delete, progress, and terminal states survive + refresh/restart as defined by the core snapshot; +6. label failure preserves the draft and retry path; +7. empty, populated, busy, error, long-name, and destructive-confirmation + states are rendered and visually inspected; +8. ordinary Invitation Transfer flows remain unchanged.