{"owner":"HapeLee","repo":"legado-with-MD3","hasSkills":true,"totalSkillsCount":17,"totalTokensCount":23370,"categories":["anthropic-skill","copilot-instructions","root-instruction","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/legado-compose-migration/SKILL.md",".agents/skills/legado-compose-migration/references/project-patterns.md",".agents/skills/legado-compose-review/SKILL.md",".agents/skills/legado-compose-review/references/review-checklist.md",".agents/skills/legado-kmp-migration/SKILL.md",".agents/skills/legado-kmp-migration/agents/openai.yaml",".agents/skills/legado-kmp-migration/references/renderer-host-strategy.md",".agents/skills/legado-kmp-migration/references/slice-checklist.md",".codex/skills/legado-compose-migration/SKILL.md",".codex/skills/legado-compose-migration/agents/openai.yaml",".codex/skills/legado-compose-migration/references/project-patterns.md",".codex/skills/legado-compose-review/SKILL.md",".codex/skills/legado-compose-review/agents/openai.yaml",".codex/skills/legado-compose-review/references/review-checklist.md",".github/copilot-instructions.md","AGENTS.md","CLAUDE.md"],"skills":{".agents/skills/legado-compose-migration/SKILL.md":"---\nname: legado-compose-migration\ndescription: Create or migrate a Legado Android screen with Jetpack Compose while preserving behavior and following the repository's Feature ownership, UDF, navigation, DI, inset, and compatibility boundaries. Use for XML/View/RecyclerView/DialogFragment migrations and new Android Compose destinations. Use legado-compose-review instead for review-only requests, and legado-kmp-migration when the change crosses a multiplatform or Gradle boundary.\n---\n\n# Legado Compose Migration\n\n## Purpose\n\nDeliver one working Android UI surface without importing View-era ownership into new Compose code.\nFor migrations, preserve observable behavior before cleanup. For new screens, use the repository's\ncurrent Feature-first and UDF conventions.\n\nBefore editing, read `AGENTS.md`, the relevant part of `docs/dev/feature-first-structure.md`, the\ncurrent implementation and one or two nearby examples that exercise the same behavior. Read\n[references/project-patterns.md](references/project-patterns.md) for repository-specific placement,\nstate and host details; examples are evidence, not templates to copy blindly.\n\n## Decide the shape\n\n- **New destination:** prefer a `MainActivity` Navigation 3 destination. Do not create a standalone\n  Activity without an external-entry or platform lifecycle reason.\n- **Migrated destination:** retain an Activity only as a thin compatibility host when existing\n  callers require stable Android Intent extras/results or a framework contract.\n- **Partial migration:** keep mature View rendering or platform integrations as explicit islands\n  when replacing them would add behavioral risk unrelated to the request.\n- **Multiplatform boundary:** stop treating this as only an Android screen migration and also use\n  `legado-kmp-migration`.\n\n## Workflow\n\n1. Bound the surface and success criteria. Record inputs, navigation/results, empty/loading/error\n   states, actions, dialogs/sheets, persistence, back behavior and external effects that must\n   remain.\n2. Read the smallest complete slice: UI, state owner, host/route, DI, resources, data/use-case\n   calls,\n   and any old implementation needed as a behavior reference.\n3. Choose the lowest state owner that reads and writes each value. Put durable business/render state\n   in `UiState`; keep purely local focus, expansion, animation and draft state in the UI when no\n   higher owner needs it. Route Feature/business actions through the Feature intent type. A direct\n   user action that only requests host navigation may use a host callback. Dialog/sheet visibility\n   is UI state; Android framework dialogs, permissions, file pickers and launches stay in the host.\n4. Keep Screen/Content free of repository, DAO, network, storage and service access. New\n   presentation\n   code uses Repository/Gateway/UseCase boundaries. Preserve an existing violation only when the\n   task is explicitly UI-only, do not spread it, and record the remaining debt.\n5. Wire navigation, DI and compatibility behavior at the host boundary. Keep public extras, result\n   codes and deep-link semantics stable unless the user requested an API change.\n6. Delete XML, adapters, bindings and resources only after references and compatibility callers are\n   gone.\n7. Verify in proportion to risk and report both passed evidence and unverified manual/device paths.\n\n## Project constraints\n\n- New ownership lives under `io.legado.app.feature.<name>`; legacy `ui/...` is a migration source,\n  not the destination for a second implementation.\n- Use ViewModel-owned read-only `StateFlow` and one `onIntent` dispatcher for Feature/business\n  actions, as required by `AGENTS.md`. When a Feature has best-effort transient effects, expose the\n  project's `SharedFlow(extraBufferCapacity = 16)` shape. Buffer capacity does not make emissions\n  durable while no UI is collecting: navigation/result/destructive completion and other\n  consistency-critical ViewModel outcomes must reduce to state or use an explicit acknowledgement\n  protocol instead of relying on an effect stream.\n- Annotate Compose-facing `UiState` and UI item models with `@Stable` only while honoring the\n  annotation's contract: public mutable properties and unobservable mutation make it incorrect.\n  Use immutable collections at the rendering boundary; do not change repository/data collections\n  mechanically.\n- On Android, collect `Flow` UI state with `collectAsStateWithLifecycle` unless a documented\n  lifecycle requirement calls for something else. A `LaunchedEffect` collector is scoped to the\n  composition, not automatically to `STARTED`/`RESUMED`. Long-lived effects must use appropriate\n  keys and `rememberUpdatedState` for changing values that should not restart them.\n- Reuse project theme and components when their behavior fits. A wrapper's existence is not enough\n  reason to use it when semantics, accessibility or host requirements differ.\n- Insets are an ownership decision. Inspect `Scaffold` `contentWindowInsets`, whether content\n  consumes `innerPadding`, nested scaffolds/sheets and IME behavior. Do not assume that using\n  `Scaffold` alone proves edge-to-edge correctness, and do not double-apply system-bar padding.\n- Preserve predictive-back behavior. Use ordinary `BackHandler` only for binary interception; use\n  `PredictiveBackHandler` when custom UI needs gesture progress. If Navigation 3 transitions are\n  customized, define and verify predictive-pop behavior as well as the normal pop transition.\n- The app targets API 37, where orientation, aspect-ratio and resizability restrictions no longer\n  protect layouts on large screens. New or substantially migrated destinations must tolerate\n  compact, medium and expanded resizable windows, rotation and recreation. Base layout decisions on\n  the current app window, not physical-device assumptions; do not add an adaptive library or\n  multi-pane layout unless the screen's behavior benefits from it.\n- Prefer Material/Foundation interactions for their semantics, focus and keyboard behavior. Custom\n  controls and gestures must provide appropriate role/state/action semantics, keyboard/D-pad access\n  and an adequate touch target.\n- Keep localized user-facing text in resources and retain fallback behavior.\n\n## Verification\n\n- Kotlin-only presentation/host changes: run `:app:compileAppDebugKotlin` using the repository's\n  Gradle wrapper syntax for the current shell.\n- Resource, manifest, XML/binding deletion or packaging changes: run `:app:assembleAppDebug`.\n- State/business changes: run focused ViewModel/use-case tests or add a characterization seam.\n- Navigation/Intent/results/insets/back: manually or instrumentedly exercise every retained entry\n  path that compilation cannot prove.\n- New or substantially changed layouts: exercise representative compact and expanded resizable\n  windows, rotation/recreation and IME interaction rather than validating only one portrait phone.\n- Always run `git diff --check`.\n\nUse actual task names from this checkout; do not invent tasks from this skill.\n",".agents/skills/legado-compose-migration/references/project-patterns.md":"# Legado Android Compose Project Patterns\n\nRead this reference while implementing a screen. Confirm every referenced API against the current\ncheckout because the repository is mid-migration.\n\n## Ownership and placement\n\n- Android application module: `:app`.\n- Canonical new Feature package: `io.legado.app.feature.<name>`.\n- `ui/...` is legacy/migration territory. Keep only explicit compatibility owners there after a\n  Feature establishes its canonical package.\n- `MainActivity` owns new in-app Navigation 3 destinations and the root graph.\n- A retained Activity translates stable external/legacy Intent inputs and results; it is not a\n  second presentation or navigation owner.\n- App-level Koin aggregation remains in the host. Use the repository's existing `viewModelOf` or\n  parameterized `viewModel` convention rather than adding a second DI pattern.\n\nA Feature may contain Contract, ViewModel, Route, Screen, components, dialogs, sheets and\npresentation models, but create only the files its behavior needs. Directory shape is not an\nacceptance criterion.\n\n## State and effects\n\nFor a behavior-heavy new screen, the repository expects:\n\n- `@Stable` Compose-facing `UiState` and UI item models;\n- immutable collections at the Compose rendering boundary;\n- private `MutableStateFlow`, exposed as read-only `StateFlow`;\n- when best-effort transient effects are actually needed, private\n  `MutableSharedFlow(extraBufferCapacity = 16)`, exposed as read-only `SharedFlow`;\n- one `onIntent` dispatcher for user actions;\n- host actions expressed as effects or callbacks.\n\n`@Stable` is an assertion, not a magic optimization. Every public property must remain stable and\nchanges observed by Compose. Wrap unstable data only when the wrapper has correct equality and\nmutation semantics. Use measurement/compiler reports before inventing performance abstractions.\n\nKeep source-of-truth business state in the ViewModel. Local `remember`/`rememberSaveable` is\nsuitable\nfor UI affordances and restorable drafts/IDs, not a second copy of repository state. Avoid\nUI-to-ViewModel feedback loops; restore consistency in the reducer or flow that produces the state.\n\nClassify host actions by delivery semantics:\n\n- A user click whose only meaning is UI navigation can call a host callback directly; use a\n  lifecycle-aware rapid-click guard such as `dropUnlessResumed` where the available Lifecycle\n  version supports it.\n- A ViewModel outcome that must survive a missing collector becomes state with an acknowledgement\n  or another explicit durable protocol.\n- Snackbar/toast/haptic feedback that is intentionally best-effort may use the Feature effect\n  stream. Document the loss behavior rather than assuming `extraBufferCapacity` solves it.\n\n## Host boundaries\n\nKeep these at the route/host unless the project already has a tested abstraction:\n\n- navigation and result delivery;\n- permission and Activity Result launchers;\n- file/document pickers;\n- Android framework dialogs and services;\n- Context-dependent clipboard, URI and external-app operations.\n\nReusable Screen functions receive state and semantic callbacks. They do not know an Activity,\nbinding, DAO, application singleton or root back stack.\n\n## Lists, lifecycle and recomposition\n\n- Use stable keys when item identity survives insertion, removal or reorder; use `contentType` when\n  heterogeneous reuse matters.\n- Collect Android route state with `collectAsStateWithLifecycle` unless another lifecycle is\n  deliberate and documented. Apply the same delivery analysis to effect collectors; composition\n  lifetime and host `RESUMED` state are not interchangeable.\n- Key `LaunchedEffect` by the lifetime it represents. Use `rememberUpdatedState` for changing\n  callbacks captured by an effect that should not restart.\n- Derive cheap display values in composition; memoize or move work only when its cost/lifetime\n  warrants it.\n- Prefer immutable Compose-facing collections, while leaving temporary computations and data-layer\n  APIs in their natural collection types.\n- Strong Skipping is enabled by default on modern Kotlin/Compose compiler versions. Unstable\n  parameters are compared by identity, so avoid needless instance churn, but do not add wrappers or\n  `@Stable` solely from intuition. The repository requires the annotation on UI state/item types;\n  those types must actually satisfy its equality and observable-mutation contract.\n\n## Insets and back\n\nTrace which layer owns each inset. For a Material 3 `Scaffold`, inspect its configured\n`contentWindowInsets` and verify that content applies/consumes the provided padding correctly.\nSheets, dialogs, IME and nested scaffolds may need separate treatment. Visual/manual evidence is\nrequired; the presence of `Scaffold`, `safeDrawing` or padding modifiers alone proves nothing.\n\nNavigation 3 integration does not remove the need to verify custom back interception, selection\nmode, unsaved changes and retained Activity entry points. Route back actions through the same owner\nthat decides whether leaving is allowed. Use `PredictiveBackHandler` when gesture progress drives\nUI;\nwhen customizing `NavDisplay` transitions, also supply/test predictive-pop behavior.\n\n## Adaptive layouts and accessibility\n\nBecause this app targets API 37, large-screen orientation, aspect-ratio and resizability\nrestrictions\ncannot be used as a compatibility fallback. Treat the current app window as dynamic across rotation,\nfold/unfold, split-screen and desktop windowing.\n\n- Test compact and expanded widths for new or substantially migrated destinations.\n- Use current window metrics/window size classes for layout decisions; do not branch on a physical\n  device category or assume portrait.\n- Preserve important input/draft/selection state across recreation with the appropriate local\n  saveable state or `SavedStateHandle`, based on the state owner and size.\n- Do not stretch a phone layout indefinitely. Adopt list-detail/supporting panes or adaptive\n  navigation only when the Feature benefits; avoid adding a library merely to satisfy a checklist.\n- Prefer standard interactive components/modifiers. Custom pointer input needs semantic actions,\n  focus/keyboard access and a usable touch target.\n\n## Architecture boundary\n\nNew UI and ViewModels do not add DAO, `appDb`, network-client or old preference access. Use existing\nGateway/Repository/UseCase contracts, or add the smallest real boundary with an actual caller.\n\nFor a strictly UI-only migration, an existing presentation-layer violation may remain to avoid\ncombining architecture and UI rewrites. Freeze rather than duplicate it, document it, and do not\ndescribe the screen as fully modernized until the boundary is corrected.\n\n## Verification selection\n\n- Kotlin-only: `:app:compileAppDebugKotlin`.\n- Resources/manifest/XML/generated binding/package: `:app:assembleAppDebug`.\n- Changed state transitions: focused unit tests with success/failure/cancellation cases as relevant.\n- Navigation or compatibility: exercise the MainActivity route and every retained Intent/result\n  entry.\n- Insets, IME, accessibility and predictive back: device/emulator evidence where risk warrants it.\n- New/substantially changed destinations: representative compact and expanded window checks plus\n  rotation/recreation; add targeted adaptive UI tests when layout branching is meaningful.\n\nUse `AGENTS.md` for the canonical full verification set and exact wrapper command.\n\n## Current official references\n\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting)\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle)\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events)\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n- [Compose side effects](https://developer.android.com/develop/ui/compose/side-effects)\n- [Material Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n- [Adaptive layouts for resizable apps](https://developer.android.com/develop/adaptive-apps/guides/app-orientation-aspect-ratio-resizability)\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n",".agents/skills/legado-compose-review/SKILL.md":"---\nname: legado-compose-review\ndescription: Review existing Legado Android Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, and compatibility hosts for concrete behavior, lifecycle, state ownership, architecture, inset, accessibility, and maintainability risks. Use for review or audit requests. Do not rewrite code unless fixes are requested; use legado-compose-migration for implementation and legado-kmp-migration for multiplatform or Gradle-boundary findings.\n---\n\n# Legado Compose Review\n\n## Purpose\n\nFind concrete defects and risky drift before proposing rewrites. Judge the code against current\nrepository behavior and constraints, not a preferred template.\n\nRead `AGENTS.md`, the scoped files and\n[references/review-checklist.md](references/review-checklist.md). Consult an old View implementation\nonly when it remains a caller or behavior baseline.\n\n## Review workflow\n\n1. Define the exact surface and whether the user asked for review only or fixes. Review-only work\n   does not authorize edits.\n2. Inspect the smallest complete behavior slice: Screen, state owner/contract, route/host, DI,\n   relevant repositories/use cases, resources and compatibility entry points.\n3. Trace state and effects end to end. Look for divergent owners, replayed one-shot work, lifecycle\n   mistakes, lost events, unstable identity and error/cancellation paths before style issues.\n4. Trace navigation/results, Insets/IME, back, accessibility and platform launchers through the real\n   host. Do not infer correctness from the presence of a particular API.\n5. Report findings first with tight line references, concrete impact and the smallest credible fix.\n   Separate compatibility-preserving fixes from optional redesign.\n6. If no finding exists, say so and list the important unverified runtime/device paths.\n\n## Severity\n\n- **P0/P1:** crash, data/behavior loss, broken navigation/result/entry compatibility, unsafe\n  lifecycle or concurrency, repeated destructive effect, inaccessible/obscured critical content.\n- **P2:** state ownership that can diverge, business/data access in UI, platform leakage, incorrect\n  stability contract, recurring recomposition/performance issue, or architecture drift with a real\n  maintenance cost.\n- **P3:** localized convention, naming, testability or cleanup issue without demonstrated behavior\n  or structural impact.\n\nDo not promote a missing annotation, different file split or wrapper choice to P2 without explaining\nthe observable or architectural consequence.\n\n## Boundaries\n\n- Do not require every screen to have the same number of files or abstractions.\n- Allow thin compatibility Activities and mature View/platform islands when they have a current\n  caller and one clear owner.\n- New Compose presentation must not add DAO, network, storage or service access; report existing\n  debt without turning a review into an unauthorized domain rewrite.\n- Treat `@Stable` as a contract that can itself be wrong, not as proof of stability or performance.\n- Strong Skipping is enabled by default with modern Kotlin/Compose compiler versions, so an\n  unstable parameter is not automatically a performance defect. Check identity churn, actual\n  recomposition evidence and compiler/benchmark data. A missing project-required annotation is P3\n  unless it causes a concrete architectural or runtime issue; an incorrect `@Stable` promise can be\n  P2 because it can suppress required updates.\n- Android `Flow` state should normally be collected with `collectAsStateWithLifecycle`. A\n  composition-scoped collector is not automatically lifecycle-aware merely because it uses\n  `LaunchedEffect`.\n- Review each ViewModel-originated `SharedFlow` effect by delivery semantics. Buffer capacity does\n  not make an event durable while no UI is collecting. Navigation, result delivery, payments,\n  destructive completion and other consistency-critical outcomes should normally reduce to state\n  or use an explicit acknowledgement protocol; best-effort transient feedback may remain an effect.\n- UI-only behavior should stay at the lowest UI owner. A direct user navigation callback handled by\n  the host is valid and often preferable to a ViewModel round trip; guard rapid/replayed navigation\n  with lifecycle-aware mechanisms such as `dropUnlessResumed` when the available Lifecycle version\n  supports it.\n- Insets and predictive back require ownership and runtime-path analysis; `Scaffold`, padding APIs,\n  Navigation 3 or `BackHandler` alone do not prove correctness.\n- Use `PredictiveBackHandler` when custom UI needs gesture progress; use ordinary `BackHandler` only\n  when binary interception is sufficient. When Navigation 3 transitions are customized, inspect\n  `predictivePopTransitionSpec` as well as normal pop behavior.\n- Reusing a project component is preferred only when its behavior, semantics and accessibility fit.\n- Prefer Material/Foundation interaction APIs because they provide semantics, focus and input\n  behavior. Custom gestures/components must preserve role, state/action semantics, keyboard/D-pad\n  access and an adequate touch target.\n\n## Current official baseline\n\nWhen library behavior matters, verify it against the repository version and current official docs:\n\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n  and [stability contracts](https://developer.android.com/develop/ui/compose/lifecycle).\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle).\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting).\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events).\n- [Material 3 Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n  and [Compose Insets](https://developer.android.com/develop/ui/compose/system/insets-ui).\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n  and [Navigation 3 transitions](https://developer.android.com/guide/navigation/navigation-3/animate-destinations).\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n  and [semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics).\n\nWhen implementation is requested, also read the migration skill's project patterns.\n",".agents/skills/legado-compose-review/references/review-checklist.md":"# Legado Compose Review Checklist\n\nUse only the sections relevant to the scoped surface.\n\n## Context\n\n- Confirm the canonical Feature owner and whether any legacy `ui/...` file is a compatibility owner.\n- Read Screen/Content, ViewModel/state contract, route/Activity, DI and directly used domain/data\n  contracts.\n- Read old XML/View/adapters only when needed for parity or a live caller.\n- Identify all entry points: MainActivity route, Intent/deep link, notification/widget/service or\n  Activity Result.\n\n## Behavior and state\n\n- Is there one durable state owner, or can ViewModel, Activity, `remember`, adapter and repository\n  copies diverge?\n- Is each state hoisted only to the lowest owner that needs to read and write it? Local expansion,\n  focus or animation state need not be forced into a ViewModel; business state must not be copied\n  into local Compose state.\n- Can loading, empty, error, selection, query, sorting and dialog/sheet states be reconstructed\n  after\n  recreation where required?\n- Are one-shot effects consumed once without becoming persistent replayable flags?\n- Can a ViewModel-produced effect be emitted while the route has no active collector? If losing it\n  would leave navigation, results or durable business state inconsistent, model/acknowledge it as\n  state rather than relying on `SharedFlow` buffering.\n- Are success, failure, cancellation and stale-response ordering handled?\n- Do `LaunchedEffect` keys match the intended lifetime and avoid duplicate loads/navigation/toasts?\n- Is `Flow`-backed Android UI state collected with `collectAsStateWithLifecycle`, or is there a\n  documented lifecycle reason for another collector?\n- Are mutable collections/entities exposed in a way that invalidates a claimed `@Stable` contract?\n- With Strong Skipping, are unstable parameters recreated on each update and compared by identity?\n  Ask for compiler/measurement evidence before treating ordinary recomposition as a defect.\n- Do lazy items use stable identity where insert/remove/reorder can attach state to the wrong row?\n\n## Architecture and ownership\n\n- Screen/Content renders state and emits semantic actions; it does not access DAO, repository,\n  network, storage, service or global application state.\n- New ViewModel code uses Gateway/Repository/UseCase boundaries and does not add DAO or old global\n  access.\n- Host owns navigation, framework launchers, permissions, Context/URI work and root lifecycle.\n- A retained Activity translates compatibility inputs/results without becoming a second business or\n  navigation owner.\n- Feature code does not depend on another Feature implementation.\n- New files do not extend a legacy package after a canonical Feature owner exists.\n\n## Navigation and compatibility\n\n- New in-app destinations are registered in the current MainActivity graph unless an external-entry\n  requirement justifies an Activity.\n- Existing extras, result codes, deep links and callers preserve semantics.\n- Multiple entry paths initialize equivalent state and converge on the same owner.\n- Nested UI requests navigation through callbacks/effects rather than mutating the root stack.\n- Pure UI navigation can call a host callback directly. Check rapid taps and lifecycle state; use a\n  lifecycle-aware guard such as `dropUnlessResumed` where supported rather than inventing a\n  ViewModel effect solely to move the navigation call.\n- Back interception covers real selection/unsaved state without breaking predictive back or host\n  result delivery.\n- Custom gesture-progress UI uses `PredictiveBackHandler`; customized Navigation 3 pop transitions\n  also define/verify predictive-pop behavior.\n\n## UI, Insets and accessibility\n\n- Trace `Scaffold` `contentWindowInsets`, inner padding consumption, nested scaffolds/sheets, system\n  bars and IME. Check for both overlap and double padding.\n- User-facing strings retain localization and fallback behavior.\n- Project components/theme are used when their semantics fit; wrappers are not applied mechanically.\n- Touch targets, focus order, semantics/content descriptions and keyboard behavior fit the control.\n- Custom pointer input has an accessible semantic action and keyboard/D-pad alternative; standard\n  Material/Foundation interactions are preferred when their behavior fits.\n- Lists/images preserve existing cache, loading and accessibility behavior where relevant.\n- Removed XML/binding/menu resources have no remaining references.\n\n## Verification and output\n\nTie each finding to evidence and the smallest fix. Recommend only checks that can observe the risk:\n\n- compilation for Kotlin wiring;\n- assemble for resources, manifest, XML/binding and packaging;\n- focused unit tests for reducers/ViewModels/use cases;\n- route plus retained Intent/result path checks for navigation compatibility;\n- device/emulator checks for Insets, IME, accessibility, predictive back and rendering performance.\n\nUse P0/P1/P2/P3 from the skill. If there are no findings, explicitly list what was not executed or\nobserved.\n",".agents/skills/legado-kmp-migration/SKILL.md":"---\nname: legado-kmp-migration\ndescription: Plan, implement, or review Legado KMP-first modularization across shared domain, data, and presentation code with independently chosen Android, Desktop, Windows-native, and optional iOS renderers. Use for Gradle boundaries, commonMain extraction, platform capabilities, native or process bridges, selective CMP, target gates, and legacy-owner removal. Do not use for an Android-only View-to-Compose rewrite unless it also changes a multiplatform or Gradle boundary.\n---\n\n# Legado KMP-first Migration\n\n## Purpose\n\nMove one verified responsibility toward a reusable KMP boundary without weakening Android behavior\nor forcing every host to use Compose. Optimize for shared behavior and explicit platform contracts,\nnot maximum shared UI percentage.\n\nValid renderers include Android Compose/View islands, Compose Desktop with Material or Fluent,\nWindows-native WinUI 3, and optional SwiftUI/UIKit. CMP is one renderer technology, not the\narchitecture root.\n\nBefore acting, read repository `AGENTS.md`, `docs/dev/kmp-cmp-modernization.md`, and the relevant\npart of `docs/dev/feature-first-structure.md`.\n\nRead supporting references only when applicable:\n\n- Implementation or review: [references/slice-checklist.md](references/slice-checklist.md).\n- Multiple renderers, WinUI 3, SwiftUI/UIKit, native export or IPC:\n  [references/renderer-host-strategy.md](references/renderer-host-strategy.md).\n\n## Choose the work mode\n\n- **Architecture/plan:** inventory current dependencies and consumers, then choose the sharing\n  boundary, renderer strategy, targets, gates and rollback point.\n- **Pure KMP extraction:** move stable models, rules, ports, use cases, reducers or stores into\n  Compose-free `commonMain`.\n- **Data/runtime extraction:** separate domain contracts from storage, network, files and rule\n  engines while retaining compatibility semantics under contract tests.\n- **Renderer boundary:** split renderer-neutral presentation from Material/Miuix, Fluent, WinUI 3,\n  SwiftUI/UIKit or specialized reader UI.\n- **Selective CMP:** share a Compose Screen/resource set only when real hosts deliberately choose\n  the same renderer and interaction model.\n- **Native host bridge:** expose a small, versioned API through an Apple framework, Windows DLL/C\n  ABI, or process/IPC boundary; do not export the internal Kotlin object graph.\n- **Build logic:** prove a target/convention/gate change with one representative module before\n  broader rollout.\n- **Review:** report findings and unverified targets first; edit only when fixes or plans were\n  requested.\n\n## Decisions required before implementation\n\n1. **What is shared?**\n    - Default candidates are domain values, business rules, repository ports, use cases,\n      serialization models, state snapshots, commands and reducers.\n    - Screens, design-system components, navigation renderers, platform ViewModel owners, resource\n      handles, lifecycle and OS integration are not shared by default.\n    - A pure presentation API must not expose Compose annotations/types, Material/Fluent types,\n      Android resources, Koin, Room, or platform SDK objects.\n\n2. **Who renders it?**\n    - Name each committed host and renderer separately; “Desktop” is not a renderer.\n    - Different renderers may consume the same presentation contract while owning distinct layouts,\n      resources, navigation and accessibility.\n    - Do not invent universal UI wrappers across unrelated design systems. Share semantic values\n      only\n      when their meaning is genuinely common.\n\n3. **How does a non-Kotlin host consume it?**\n    - JVM Compose hosts can depend on KMP/JVM modules directly.\n    - SwiftUI/UIKit needs a deliberately exported Apple framework facade.\n    - WinUI 3 needs either a Kotlin/Native `mingwX64` DLL with a narrow C ABI or a separately\n      packaged\n      JVM process reached through a versioned local IPC protocol.\n    - Keep rich Kotlin types internal. Define DTO/state snapshots, commands, errors, cancellation,\n      callback threads, allocation ownership and disposal at the bridge.\n\n4. **What evidence supports the claim?**\n    - Distinguish metadata/compile, contract-test, host smoke, package and release-ready evidence.\n    - Android plus JVM compilation does not prove iOS or Windows Native readiness.\n    - Do not document a module, target, task or host as current until it exists in this repository.\n\n## Workflow\n\n1. Bound one slice: exact files, callers, behavior, intended owner/source set, host impact and\n   rollback point.\n2. Classify dependencies as `common-ready`, `contract-needed`, `renderer-specific`,\n   `platform-island`, or `unknown`. Imports are only a first pass; compile every claimed target.\n3. Establish characterization or contract tests before moving behavior. Preserve storage, script\n   ABI, serialization, error, cancellation, ordering and threading semantics.\n4. Choose the smallest seam. Prefer interfaces plus constructor injection; use `expect/actual` only\n   for true platform primitives or a documented case where injection cannot provide the boundary.\n5. Implement additively, migrate a bounded caller set, then delete the old owner when no callers\n   remain. Do not keep internal `Help/Utils/Base/Provider` facades for import compatibility.\n6. Run actual repository gates and target tasks. Never infer task support from the target design.\n7. Report behavior evidence, dependency/baseline delta, capability changes, exact commands,\n   rollback path and unverified hosts/devices.\n\n## Boundary rules\n\n- Pure KMP domain/presentation does not depend on Compose, AndroidX ViewModel, Room entities/DAO,\n  Koin, platform resources, `File`/URI, JVM-only libraries or renderer types.\n- Renderer modules may depend on presentation; presentation never depends on a renderer.\n- Each host owns its composition root, root navigation/window lifecycle, platform effects,\n  implementation selection and packaging.\n- Unsupported capabilities are explicit; a successful no-op is not an implementation.\n- Feature-to-Feature implementation dependencies remain forbidden. Create `api/impl` only for a\n  real Gradle consumer; split renderers by responsibility instead of generic layering names.\n- Do not change storage, network, DI, navigation and UI technology in one slice.\n- Historical baselines only decrease. A new source set starts at zero and cannot justify raising a\n  baseline.\n\n## Verification minimums\n\n- Always retain the affected Android G0 gates.\n- Pure KMP: common tests plus every claimed target compile.\n- Data/runtime: adapter contracts and compatibility checks proportional to storage, serialization,\n  cancellation and error risks.\n- CMP renderer: target compile plus rendered UI/semantics smoke and runtime dependency alignment.\n- WinUI 3 DLL: native link, generated API review, consumer smoke, memory/disposal/error/thread tests\n  and package loading.\n- WinUI 3 IPC: protocol compatibility, startup/shutdown/reconnect, local-access policy,\n  cancellation and installer smoke.\n- iOS native UI: framework export plus a Swift consumer compile/observation/lifecycle smoke.\n- Always run `git diff --check`; use a clean rebuild after cross-module moves.\n\n## Review output\n\nList findings by impact with tight file/line evidence and the smallest credible fix:\n\n- P0/P1: behavior or data loss, ABI/storage incompatibility, broken target, lifecycle/thread/\n  cancellation defect, or package/runtime failure.\n- P2: renderer leakage into presentation, illegal dependency, false capability, untested bridge,\n  state duplication, baseline relaxation, or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf there are no findings, state which host, package, device, renderer, interop and performance paths\nremain unverified.\n",".agents/skills/legado-kmp-migration/agents/openai.yaml":"interface:\n  display_name: \"Legado KMP-first Migration\"\n  short_description: \"Plan shared KMP logic and independent native or Compose renderers\"\n  default_prompt: \"Use $legado-kmp-migration to plan a reviewable KMP-first slice with explicit host, renderer, interop, and verification boundaries.\"\n",".agents/skills/legado-kmp-migration/references/renderer-host-strategy.md":"# Renderer and Host Strategy\n\nRead this reference when a Feature may use more than one UI stack or when a non-Kotlin host consumes\nshared Kotlin behavior.\n\n## Separate three decisions\n\n1. **Shared behavior:** domain, repositories, use cases, state snapshots, commands and reducers.\n2. **Host integration:** Gradle/JVM, Apple framework, Windows DLL/C ABI, or process/IPC.\n3. **Renderer:** Android Material/Miuix, Compose Material/Fluent, WinUI 3, SwiftUI/UIKit, or a\n   specialized platform island.\n\nDo not infer one decision from another. A KMP core does not require CMP, and Compose on one host\ndoes\nnot require every host to share the same Screen.\n\n## Renderer-neutral presentation\n\nPrefer immutable state snapshots, stable IDs, commands/intents, effects, domain values and explicit\ncapability/error states. Keep Composable lambdas, `Modifier`, Material/Fluent types,\n`StringResource`, icons/painters, navigation UI objects, Android Context, Swift/WinRT objects, Room\nentities and DI containers out of the boundary.\n\n`@Stable` belongs to a Compose-facing contract or adapter. If WinUI 3 or SwiftUI also consumes the\nstate, keep the pure state free of renderer annotations and adapt it at the Compose boundary.\n\n## WinUI 3 choices\n\nWinUI 3 cannot consume a Kotlin/JVM Gradle module like a Kotlin host. Choose the bridge explicitly.\n\n### Kotlin/Native DLL and C ABI\n\nUse a `mingwX64` shared library when in-process calls and one packaged process matter, and only when\nthe required dependency closure supports the target.\n\n- Export a small facade, not repositories, Flow, sealed hierarchies or generic Kotlin APIs.\n- Use opaque handles with explicit create/dispose.\n- Define ownership for every returned string or buffer.\n- Map async work to request IDs plus callbacks/polling; define callback thread and cancellation.\n- Version the ABI and compile a native consumer in CI.\n- Do not force JVM-only Room, Rhino or jsoup dependencies into the DLL; split a native-compatible\n  core or choose IPC for those capabilities.\n\n### JVM sidecar and IPC\n\nKeep the existing JVM data/runtime stack in a separately packaged process and expose a versioned\nlocal protocol when reuse is more important than in-process integration.\n\n- Define protocol DTOs and version negotiation.\n- Define startup, readiness, shutdown, crash recovery and upgrade behavior.\n- Restrict local access; do not unintentionally expose an unauthenticated network service.\n- Propagate cancellation and structured errors.\n- Package and test both processes as one product.\n\nUse one bounded read/write Feature to compare cold start, call latency, state mapping, database and\nruntime reuse, memory ownership, crash isolation, installer complexity and test ergonomics before\nchoosing the product-wide bridge.\n\n## iOS native renderer\n\nExport a deliberately small Apple framework facade. SwiftUI may observe a tested shared state host\nor own an `ObservableObject` that calls shared repositories/use cases. Verify Swift names,\noptionality, collections, async/Flow bridging, lifecycle, cancellation and memory ownership with a\nSwift consumer test.\n\n## Selective CMP\n\nShare a CMP Screen only when real hosts intentionally share its visual and interaction model.\nResources and design-system components belong to that renderer. If Desktop uses WinUI 3 and iOS\nuses SwiftUI, Android Compose can remain an Android renderer even when its code is technically\nportable.\n",".agents/skills/legado-kmp-migration/references/slice-checklist.md":"# KMP Slice Checklist\n\nUse this reference for implementation plans, extraction work, scaffolding or reviews. The default\npath is renderer-neutral KMP. CMP checks apply only after a shared Compose renderer has been chosen;\nthey are not instructions to convert every Feature or host to Compose.\n\n## Before change\n\n- Scope names exact files/packages, current callers, one boundary and one rollback point.\n- Existing behavior is captured by tests or a written parity list.\n- The target owner, source set and consumers are real rather than copied from the target diagram.\n- Dependencies are classified as common-ready, contract-needed, renderer-specific,\n  platform-island or unknown.\n- Actual Gradle target and verification task names have been inspected.\n\n## Contract quality\n\n- Public types express domain values instead of Android/JVM/storage/rendering details.\n- Error, cancellation, threading, transaction, ordering, serialization and ownership semantics are\n  explicit where they cross the boundary.\n- Capability absence is visible to callers.\n- An ordinary interface is used unless `expect/actual` provides a concrete static benefit.\n- Every abstraction has a real caller and removes a measurable dependency.\n- Native or IPC exports are smaller and more stable than the internal Kotlin API.\n\n## Module and source-set graph\n\n- The app host owns implementation aggregation, DI, navigation/window lifecycle and packaging.\n- Core does not import Feature; Feature does not import another Feature implementation.\n- Shared modules do not depend on platform implementations or renderer modules.\n- Gradle `api` exposure is intentional; otherwise use `implementation`.\n- Package colocation, Android module extraction, KMP conversion and renderer sharing remain separate\n  changes unless evidence requires combining them.\n- `commonMain` is compiled by at least one non-Android target before being called shared.\n- JVM+Android-only sharing has an honest owner and is not mislabeled as common.\n- Platform files live in the narrowest relevant source set.\n- Android services, Context/URI/resources and notifications remain Android-side.\n- Rhino/JS behavior remains behind a capability boundary until another target has a compatible,\n  tested implementation.\n\n## Renderer and host checks\n\n- Presentation contracts contain no Compose, Material/Miuix, Fluent, WinUI, SwiftUI or platform\n  resource types.\n- A renderer owns its resources, accessibility, layout and navigation presentation.\n- Shared Compose UI emits semantic callbacks/effects; the host owns platform launchers.\n- A CMP Screen has at least two intentional consumers or another documented product reason.\n- WinUI/iOS bridges define versioning, lifecycle, cancellation, error and memory ownership.\n- A sidecar protocol defines startup, readiness, shutdown, crash recovery, upgrade and local-access\n  behavior.\n\n## Gates\n\n- Affected Android unit/lint/architecture/package gates pass.\n- Common tests and actual metadata/target compile tasks pass.\n- Capability status distinguishes compile, contract-test, smoke, package and release-ready.\n- Adapter tests cover success, failure and cancellation where relevant.\n- Serialization/database changes include backward/forward or migration evidence.\n- Reader/rule/service changes include parity and, where relevant, real-device performance evidence.\n- Reduced historical violations lower their baseline in the same change.\n- `git diff --check` passes.\n\n## Scaffolding\n\n- At least two accepted manual examples prove a convention before it is generated.\n- Dry-run is available and default execution refuses overwrites.\n- Existing graph/DI files are amended safely rather than replaced wholesale.\n- Only necessary files are generated; no empty layers or speculative targets.\n- Generator behavior has fixture, snapshot or compilation evidence.\n\n## Review output\n\nFor each finding give a tight file/line reference, concrete impact and smallest credible fix:\n\n- P0/P1: behavior/data loss, incompatible rule/storage/ABI semantics, broken target, lifecycle,\n  thread, cancellation, native ownership or packaging defects.\n- P2: illegal dependency, renderer/platform leakage, false capability, state duplication, baseline\n  relaxation or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf no issue is found, state the remaining unverified host, device, package, interop and performance\nrisks.\n",".codex/skills/legado-compose-migration/SKILL.md":"---\nname: legado-compose-migration\ndescription: Guide Legado Android UI migration from XML/View/RecyclerView/DialogFragment screens to Jetpack Compose and Material 3, and guide new Compose-first screens using standard Android architecture. Use when Codex is asked to create, migrate, rewrite, review, or plan a Legado screen, Activity, Fragment, dialog, adapter, navigation destination, or settings page, especially when MainActivity navigation, MVI/UDF, StateFlow, Koin ViewModels, domain/usecase boundaries, or existing Compose component conventions matter.\n---\n\n# Legado Compose Migration\n\n## Overview\n\nMigrate one UI surface at a time, or create one new Compose destination at a time. Preserve behavior first for migrations; for newly created screens, prefer standard modern Android architecture over mixed legacy patterns. Use the project's existing `MainActivity` navigation, `BaseComposeActivity` compatibility hosts, `*Screen`, `*Contract`, `*ViewModel`, `StateFlow`, `SharedFlow`, Koin, theme, and widget patterns.\n\nBefore editing, inspect the target View implementation if one exists, `MainActivity` navigation when adding a destination, and at least two nearby migrated Compose screens. For concrete project patterns, including current Compose state/performance rules, read `references/project-patterns.md`.\n\n## Workflow\n\n1. State assumptions and success criteria.\n   - Name the exact screen or destination being created/migrated.\n   - For migrations, define behavior that must remain unchanged: inputs, result codes, navigation, menu actions, dialogs/sheets, list selection, refresh, persistence, and event bus behavior.\n   - For new screens, define the route owner, UI state, events/effects, domain/usecase boundary, and verification target.\n   - If the target mixes UI and business logic heavily, keep the migration surgical and defer deeper domain cleanup unless needed.\n\n2. Map the old surface.\n   - For migrations, read the Activity/Fragment, XML layouts, adapters, menu XML, dialogs, result launchers, and ViewModel.\n   - For new screens, read `MainActivity`, nearby route screens, the relevant ViewModel/usecase/repository patterns, and shared UI components.\n   - List UI state, user intents, one-shot effects, and external side effects.\n   - Identify reusable Compose components under `ui/widget/components` before creating new components.\n\n3. Choose the minimal migration shape.\n   - For new Compose-first screens, add a `MainActivity` navigation destination instead of creating a standalone Activity.\n   - Use `BaseComposeActivity` for full-screen Activity migrations only when a legacy Activity must remain as an entry point.\n   - Keep existing Activity Result APIs, `Intent` extras, permission flows, and Android framework calls in the host/compatibility Activity.\n   - If an unreworked View screen still starts the migrated screen with `Intent`, keep the old Activity only as a compatibility host that parses legacy extras and delegates to the Compose screen or `MainActivity` route boundary.\n   - Put renderable state in `UiState`, user actions in `Intent`, and one-shot navigation/framework work in `Effect`.\n   - For new screens, use standard Android/Compose architecture: UDF/MVI-style state hoisting, lifecycle-aware Flow collection, ViewModel-owned state, repository/usecase boundaries, and UI free of business logic.\n   - Use mixed legacy patterns only as an integration boundary for unreworked View screens or existing framework contracts.\n   - Use existing repositories/usecases when they already fit; introduce new domain/usecase classes when a new screen needs clean business boundaries or when a migration would otherwise duplicate or entangle business logic.\n\n4. Implement by layers.\n   - Create or update `FeatureContract.kt` first for `UiState`, `Intent`, `Effect`, dialog/sheet models, and menu action enums.\n   - Update `FeatureViewModel.kt` to expose `uiState: StateFlow<UiState>` and `effects: SharedFlow<Effect>`, with a single `onIntent(...)` entry point unless the existing feature has a simpler established pattern.\n   - Create `FeatureScreen.kt` as a stateless route-level composable: `state`, callbacks, and `onIntent`.\n   - Wire new Compose destinations through `MainActivity` route handling; update a retained Activity only when legacy `Intent` compatibility is required.\n   - Collect state with `collectAsStateWithLifecycle()` and collect effects in `LaunchedEffect(Unit)` from the route or compatibility host.\n   - Register new ViewModels in `di/appModule.kt` with `viewModelOf(::FeatureViewModel)` unless parameters require `viewModel { ... }`.\n\n5. Remove only obsolete migration artifacts.\n   - Delete XML layouts, adapters, menu resources, binding fields, and imports only when the migrated screen no longer references them.\n   - Do not refactor unrelated View screens or shared utilities.\n\n6. Verify.\n   - Prefer the smallest Gradle check that compiles the touched app code, usually `.\\gradlew.bat :app:compileAppDebugKotlin`.\n   - If resources, manifests, or XML deletion are involved, run `.\\gradlew.bat :app:assembleAppDebug` when feasible.\n   - For behavior-heavy changes, add or update focused tests only where the project already has a practical test seam.\n\n## Boundaries\n\n- Keep Compose functions side-effect-light. Use `LaunchedEffect` for collecting effects and use callbacks for user actions.\n- Do not pass `Activity`, `View`, binding objects, or mutable domain entities deep into composables unless an existing local pattern requires it.\n- Prefer project theme/components: `LegadoTheme`, `AppScaffold`, `ListScaffold`, `AppAlertDialog`, `AppModalBottomSheet`, `RoundDropdownMenu`, top bar helpers, setting items, cover components, and list utilities.\n- Prefer `StateFlow`/`SharedFlow` over `LiveData` for newly migrated Compose surfaces.\n- When using `FeatureIntent` for MVI user actions, distinguish it from Android `Intent` extras and launch APIs in names, comments, and explanations where both appear.\n- For new Compose-first screens, do not copy View-era shortcuts such as UI logic in Activity/Fragment, direct binding-like mutable UI state, adapter-owned state, or Activity-context business operations.\n- Keep one-off Android actions out of `UiState`: navigation, file opening, clipboard, dialogs implemented as Android DialogFragments, result launchers, permission requests, and callbacks that require host context should be `Effect`s handled by `MainActivity` route handling or a compatibility Activity.\n- Prefer `MainActivity` navigation for new Compose destinations. Treat standalone Activities for migrated screens as legacy entry points only when existing View code still depends on `Intent` navigation.\n- Keep existing Chinese string resources and localization behavior; add strings to resources when user-facing text is new.\n\n## Reference\n\nRead `references/project-patterns.md` when implementing or reviewing a migration. It contains project-specific examples, file placement rules, state/effect conventions, and verification commands.\n",".codex/skills/legado-compose-migration/agents/openai.yaml":"interface:\n  display_name: \"Legado Compose Migration\"\n  short_description: \"Guide Legado Compose screens and migration\"\n  default_prompt: \"Use $legado-compose-migration to create or migrate a Legado Android Compose screen with MainActivity navigation and standard architecture.\"\n",".codex/skills/legado-compose-migration/references/project-patterns.md":"# Legado Compose Migration Project Patterns\n\n## Existing Shape\n\n- App module: `app`.\n- Main source root: `app/src/main/java/io/legado/app`.\n- UI package: `ui/...`, usually grouped by feature.\n- Data package: `data/dao`, `data/entities`, `data/repository`.\n- Domain package: `domain/gateway`, `domain/model`, `domain/repository`, `domain/usecase`.\n- DI: `di/appModule.kt`, using Koin `singleOf`, `viewModelOf`, and parameterized `viewModel { ... }`.\n- Main navigation: `ui/main/MainActivity.kt`, using Navigation3 `NavKey`, `rememberNavBackStack`, `entryProvider`, and `NavDisplay`, plus legacy route extras such as `EXTRA_START_ROUTE`.\n- Base Compose host: `base/BaseComposeActivity.kt`, which wraps `Content()` in `AppTheme`, configures system bars, locale/font, background image, and LiveBus recreation events.\n- Compose dependencies are already enabled in `app/build.gradle.kts`; do not add new UI libraries unless the target screen truly requires one.\n- `kotlinx.collections.immutable` is available for Compose-facing screen state. Prefer `ImmutableList`, `ImmutableSet`, and `ImmutableMap` at `UiState` boundaries when collection state is passed to composables and changes often.\n\n## Files to Inspect Before Migrating\n\nInspect these as local examples, not as APIs to copy blindly:\n\n- `ui/book/info/BookInfoActivity.kt`\n- `ui/book/info/BookInfoContract.kt`\n- `ui/book/info/BookInfoViewModel.kt`\n- `ui/book/info/BookInfoScreen.kt`\n- `ui/book/search/SearchContract.kt`\n- `ui/book/search/SearchViewModel.kt`\n- `ui/book/search/SearchScreen.kt`\n- `ui/main/MainActivity.kt`\n- `ui/main/MainScreen.kt`\n- `ui/widget/components/AppScaffold.kt`\n- `ui/widget/components/list/ListScaffold.kt`\n- `ui/theme/LegadoTheme.kt`\n- `ui/theme/AppTheme.kt`\n\n## Recommended Feature Layout\n\nFor a migrated feature, prefer colocated files:\n\n```text\nui/<area>/<feature>/\n  FeatureActivity.kt       # only for legacy Intent compatibility or full-screen migration host\n  FeatureContract.kt\n  FeatureViewModel.kt\n  FeatureScreen.kt\n  FeatureSheets.kt        # only if sheets are substantial\n  FeatureDialogs.kt       # only if dialogs are substantial\n```\n\nUse the smallest set of files. Do not split files just to match this shape.\n\n## MVI and UDF Rules\n\nUse this shape for behavior-heavy screens:\n\n```kotlin\ndata class FeatureUiState(\n    val isLoading: Boolean = false,\n    val items: List<Item> = emptyList(),\n    val dialog: FeatureDialog? = null,\n    val sheet: FeatureSheet = FeatureSheet.None,\n)\n\nsealed interface FeatureIntent {\n    data object BackPressed : FeatureIntent\n    data object Refresh : FeatureIntent\n    data class ItemClick(val id: Long) : FeatureIntent\n}\n\nsealed interface FeatureEffect {\n    data object Finish : FeatureEffect\n    data class OpenDetail(val id: Long) : FeatureEffect\n}\n```\n\nViewModel rules:\n\n- Keep `_uiState = MutableStateFlow(FeatureUiState())` private and expose `uiState = _uiState.asStateFlow()`.\n- Keep `_effects = MutableSharedFlow<FeatureEffect>(extraBufferCapacity = 8)` private and expose `effects = _effects.asSharedFlow()`.\n- Prefer `fun onIntent(intent: FeatureIntent)` as the UI entry point.\n- Treat `FeatureIntent` as an MVI/user-action type. When Android `Intent` launch/extras are also involved, name variables and explanations clearly enough that the two concepts are not confused.\n- Use `_uiState.update { it.copy(...) }` for state changes.\n- Emit one-shot work through effects, not booleans in state.\n- Keep cached current entities private in the ViewModel when existing project behavior needs mutation or incremental sync, but publish immutable render state.\n- For collection-heavy `UiState`, convert DAO/repository `List`/`Set`/`Map` values to `kotlinx.collections.immutable` at the ViewModel/UI-state boundary with `toImmutableList()`, `toImmutableSet()`, or `toImmutableMap()`. Do not force repository, DAO, or domain APIs to use persistent collections unless the domain contract truly benefits.\n- Keep internal flow pipelines free to use normal Kotlin collections for sorting, grouping, and persistence work; the immutable collection rule is primarily for values exposed to Compose.\n- Use existing `BaseViewModel.execute { ... }.onSuccess { ... }.onError { ... }` when the surrounding ViewModel already uses that pattern.\n\nActivity rules:\n\n- Prefer `MainActivity` as the owner of Compose-first navigation. Add a Navigation3 route/key and `entryProvider` entry there for new destinations.\n- Extend `BaseComposeActivity` for migrated Activity screens only when a standalone Activity remains necessary.\n- Keep a legacy Activity for a migrated screen only when unreworked View code still calls it through `Intent`; in that case, make the Activity a thin compatibility host that translates extras/results and delegates to Compose state/effects.\n- Keep Activity Result launchers, file openers, clipboard, permission APIs, Android DialogFragments, and framework navigation in the Activity.\n- In `Content()`, collect state with `collectAsStateWithLifecycle().value`.\n- Collect effects in `LaunchedEffect(Unit) { viewModel.effects.collectLatest { ... } }`.\n- Pass only `state` and callbacks into the screen.\n\nNavigation rules:\n\n- For new Compose-first destinations, add the route to `MainActivity` instead of adding a new Activity.\n- Follow `MainActivity`'s current route style: serializable `NavKey` routes for in-process navigation and launcher `Intent` helper methods only when external or legacy callers need a stable entry point.\n- Keep legacy extra names stable when replacing an old Activity entry point.\n- When a compatibility Activity is retained, let it translate old `Intent` inputs/results; do not make it the source of truth for navigation or business behavior.\n\nScreen rules:\n\n- Make `FeatureScreen(state, onIntent, ...)` stateless for business state.\n- Use `remember` and `rememberSaveable` only for local UI affordances such as menu expansion, scroll state, transient animation state, and text field drafts when committing through an intent.\n- Use `rememberSaveable` for user-driven transient targets that should survive recreation, such as a delete-confirmation item ID or URL. Store only the stable ID in saved state and resolve the current entity from `UiState`.\n- Use `BackHandler` to route back actions through `FeatureIntent.BackPressed` when ViewModel decisions matter.\n- Keep expensive derived values in `remember(key)` or ViewModel state when they depend on repository data.\n- Prefer `LazyColumn`, `LazyRow`, stable keys, and existing fast-scroller/list components for adapter migrations. For heterogeneous lazy lists or grids, provide `contentType` in addition to `key`.\n- In long-lived `LaunchedEffect` collectors, wrap changing callbacks, `Context`-dependent operations, or lambdas from parents with `rememberUpdatedState` when the effect should not restart.\n- Hoist state only to the lowest common owner that reads and writes it. Move branch-only ViewModel lookup and Flow collection into the branch or a small child composable instead of collecting at a high-level route.\n- Avoid UI-state feedback loops such as `LaunchedEffect(uiState.items) { viewModel.pruneSelection(...) }`. Prefer deriving consistency inside the ViewModel with `combine(...)`, or reduce the state as part of the flow that produces the data.\n\n## Current Compose Performance Notes\n\n- Kotlin 2.x enables strong skipping by default, but ordinary Kotlin `List`, `Set`, and `Map` are still unstable to Compose. This means high-frequency screen state with normal collections can still widen recomposition work.\n- Use immutable collections for render state that crosses into Compose. Do not mechanically replace every temporary collection, Room query result, or internal mutable accumulator.\n- If data entities come from modules where the Compose compiler does not run, consider a stable UI model wrapper when the entity is passed deeply through composables and causes measurable recomposition cost.\n- Treat `SnapshotStateList` / `SnapshotStateMap` as UI-owned mutable state, not as a default ViewModel `UiState` transport type.\n\n## Clean Architecture Boundaries\n\nThis project is mid-migration, so use Clean Architecture pragmatically. For newly created screens, default to standard modern Android architecture and avoid mixed legacy patterns unless they are needed to interoperate with View screens that have not been rewritten.\n\n- For new screens, keep UI, presentation, domain, and data boundaries explicit: Compose renders state, ViewModel owns state and intent handling, usecases hold reusable business actions, repositories mediate data access.\n- For new screens, do not place business rules in Activity/Fragment or composables.\n- For new screens, do not read DAOs directly from UI/presentation unless the project already has no reasonable repository/usecase boundary and adding one would be disproportionate.\n- For migrations, use existing DAOs/repositories directly if the old ViewModel already does and the migration is UI-only.\n- Prefer existing `domain/usecase` classes for reusable business actions such as cache, delete, group update, startup maintenance, reading progress, and WebDAV flows.\n- Add a usecase when a new screen has meaningful business rules, when logic would otherwise be duplicated across features, or when a UI migration needs to extract meaningful business rules from an Activity/adapter.\n- Keep UI-only formatting and Compose layout decisions out of domain.\n- Keep entity-to-display mapping near the UI or ViewModel unless it is reused business language.\n\n## Compose Components and Theme\n\nPrefer project components before raw Material widgets:\n\n- Scaffold/top bars: `AppScaffold`, `GlassTopAppBar...`, `DynamicTopAppBar`, `TopBarActionButton`, `TopBarNavigationButton`.\n- Lists: `ListScaffold`, `TopFloatingStickyItem`, `VerticalFastScroller`, `SelectionBottomBar`.\n- Dialog/sheet: `AppAlertDialog`, `AppModalBottomSheet`, `OptionSheet`, text list input components.\n- Settings: `ClickableSettingItem`, `SwitchSettingItem`, `SliderSettingItem`, `ListSettingItem`, `SettingCard`.\n- Buttons/chips: `AppIconButton`, `SmallIconButton`, `SmallTextButton`, `ToggleChip`, `AlertButton`.\n- Covers/images: `CoilBookCover`, `BookshelfCover`, `buildCoverImageRequest`; use Coil for Compose image loading.\n- Theme: `LegadoTheme.colorScheme`, `LegadoTheme.typography`, `ThemeResolver`, `ProvideThemeOverride` where existing feature behavior needs theme override.\n\nAvoid introducing a second visual system. If a raw Material 3 component is used, style it from `LegadoTheme`.\n\n## Migration Checklist\n\nBefore editing:\n\n- Identify old XML layouts, ViewBinding fields, adapters, decorations, menu XML, dialogs, and result contracts.\n- Capture behavior: empty/loading/error states, refresh, search, sort, selection, long click, swipe, menu actions, back behavior, result codes, events, and persisted preferences.\n- Decide whether the task is UI-only or needs domain extraction.\n- For new screens, explicitly decide the presentation/domain/data boundary before coding; do not default to legacy mixed architecture.\n\nDuring editing:\n\n- Create/update `Contract`, then `ViewModel`, then `Screen`, then `MainActivity` route or compatibility host.\n- For new Compose destinations, wire navigation through `MainActivity` unless a legacy `Intent` caller must remain supported.\n- When retaining an Activity for compatibility, keep it small: parse legacy extras, collect effects, bridge result codes, and avoid placing feature business logic there.\n- Reuse existing string resources and add new resources for new user-facing text.\n- Register the ViewModel in `di/appModule.kt`.\n- Remove only resources made obsolete by this migration.\n- Keep existing public intent extra names and result codes unless explicitly changing an API.\n\nVerification:\n\n- Run `.\\gradlew.bat :app:compileAppDebugKotlin` for Kotlin-only changes.\n- Run `.\\gradlew.bat :app:assembleAppDebug` when resources, manifests, XML deletion, or generated bindings are affected.\n- If a screen uses Room queries or migration-sensitive repositories, run the relevant existing Android/unit tests if practical.\n- For manual verification, open the migrated screen and compare core flows against the old behavior list.\n\n## Common Pitfalls\n\n- Do not put navigation or Activity launchers inside composables.\n- Do not store one-time navigation as nullable fields in `UiState` unless the existing local pattern already does; prefer `SharedFlow` effects.\n- Do not keep adapter selection state split between old adapter and new Compose state.\n- Do not delete XML/menu resources until all references and generated binding imports are gone.\n- Do not add speculative domain layers for a single migrated screen.\n- Do not use mixed architecture for new screens unless compatibility with unreworked View UI requires it.\n- Do not make a migrated screen's retained Activity the primary navigation model when `MainActivity` can own the Compose destination.\n- Do not bypass `BaseComposeActivity` unless the screen must remain inside a Fragment or legacy host.\n- Do not replace existing event bus or service interactions as part of a UI migration unless required for correctness.\n",".codex/skills/legado-compose-review/SKILL.md":"---\nname: legado-compose-review\ndescription: Review existing Legado Jetpack Compose code for architecture, behavior, maintainability, and project convention issues. Use when Codex is asked to audit, review, inspect, evaluate, or find problems in Legado Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, or early Compose implementations, especially for MVI/UDF, StateFlow/SharedFlow, Clean Architecture, MainActivity navigation, legacy Activity compatibility, and View-era mixed-pattern drift.\n---\n\n# Legado Compose Review\n\n## Overview\n\nReview existing Compose code before rewriting it. Focus on concrete defects, architectural drift, behavior risks, and missing verification, especially in early Compose screens that may predate the current MVI/UDF, Clean Architecture, and `MainActivity` navigation expectations.\n\nRead `references/review-checklist.md` for the project-specific checklist, current Compose state/performance checks, and severity guidance.\n\n## Workflow\n\n1. Define the review scope.\n   - Identify the exact screen, route, ViewModel, contract, or package under review.\n   - State whether the user wants review only or review plus fixes. If unclear, review first and do not edit code.\n   - Treat unrelated legacy View code as context, not as part of the review, unless it affects the Compose surface.\n\n2. Build context from code.\n   - Read the `*Screen`, `*ViewModel`, `*Contract`, host Activity/route, DI registration, and any repositories/usecases used by the feature.\n   - Read the old View implementation only if it is still a compatibility caller or behavior reference.\n   - Compare against nearby current examples such as `BookInfo*`, `Search*`, `MainActivity`, and shared components under `ui/widget/components`.\n\n3. Review by risk, not style preference.\n   - Prioritize behavior regressions, state duplication, lifecycle bugs, navigation bugs, business logic in UI, direct data access from UI/presentation, recomposition hazards, and missing compatibility handling.\n   - Flag style-only issues only when they conflict with established project conventions or make future migration harder.\n   - Do not require broad refactors for a small screen unless the current code creates real behavior or maintenance risk.\n\n4. Output findings first.\n   - Use code-review style: list findings before summaries.\n   - Include file and tight line references.\n   - Explain the concrete impact and the smallest credible fix.\n   - If using Codex review directives, emit one `::code-comment{...}` per finding.\n   - If no issues are found, say that clearly and mention remaining test/manual verification gaps.\n\n5. Suggest fixes only after findings.\n   - Group fixes into small, reviewable steps.\n   - For architecture drift, separate compatibility-preserving fixes from larger cleanup.\n   - Recommend `legado-compose-migration` only when the finding implies a migration or rewrite workflow.\n\n## Review Priorities\n\n- **P0/P1**: behavior breakage, lost navigation/result behavior, unsafe lifecycle collection, stale state, data corruption, crashes, or compatibility entry points that no longer work.\n- **P2**: architecture drift that will cause duplicated logic, hard-to-test behavior, recomposition bugs, or incorrect ownership of state/effects.\n- **P3**: convention drift, maintainability issues, weak naming, missing previews/tests, or small cleanup that should not block behavior.\n\n## Boundaries\n\n- Do not rewrite code during a review unless the user asks for fixes.\n- Do not demand pure architecture where a compatibility boundary is necessary for unreworked View screens.\n- For new Compose-first code, expect standard Android architecture: `MainActivity` route ownership, ViewModel-owned state, UDF/MVI user actions, `StateFlow`/`SharedFlow`, repositories/usecases, and UI without business logic.\n- For migrated code, allow a thin retained Activity only for Android `Intent` compatibility with unreworked View callers.\n- Distinguish MVI `FeatureIntent` user actions from Android `Intent` launch/extras when both appear.\n\n## Reference\n\nRead `references/review-checklist.md` for detailed checks. When a review turns into implementation, also read `../legado-compose-migration/references/project-patterns.md` if available.\n",".codex/skills/legado-compose-review/agents/openai.yaml":"interface:\n  display_name: \"Legado Compose Review\"\n  short_description: \"Review Legado Compose architecture quality\"\n  default_prompt: \"Use $legado-compose-review to review a Legado Compose screen for MVI, UDF, Clean Architecture, navigation, and project convention issues.\"\n",".codex/skills/legado-compose-review/references/review-checklist.md":"# Legado Compose Review Checklist\n\n## Files to Read\n\nFor a feature review, inspect the smallest complete slice:\n\n- `FeatureScreen.kt` and any `FeatureSheets.kt` / `FeatureDialogs.kt`.\n- `FeatureViewModel.kt`.\n- `FeatureContract.kt` if present.\n- Host route in `MainActivity.kt` or retained `FeatureActivity.kt`.\n- Koin registration in `di/appModule.kt`.\n- Repositories/usecases used by the feature.\n- Old View caller or XML/adapters only when still used for compatibility or behavior comparison.\n\nUse current examples as references:\n\n- `ui/main/MainActivity.kt` for Navigation3 route ownership.\n- `ui/book/info/BookInfoContract.kt`, `BookInfoViewModel.kt`, `BookInfoScreen.kt` for behavior-heavy MVI/UDF.\n- `ui/book/search/SearchContract.kt`, `SearchViewModel.kt`, `SearchScreen.kt` for route-level search state.\n- `ui/widget/components/...` and `ui/theme/...` for shared UI conventions.\n\n## Architecture Checks\n\nFlag issues when:\n\n- A new Compose-first screen is implemented as a standalone Activity instead of a `MainActivity` destination without a compatibility reason.\n- A retained Activity contains feature business logic instead of acting as a thin Android `Intent` compatibility host.\n- Business rules, repository calls, DAO calls, persistence writes, or service orchestration live in composables.\n- ViewModel exposes mutable state directly, exposes `MutableStateFlow`, or lets UI mutate domain objects.\n- UI state is split across Activity fields, composable `remember` state, adapter state, and ViewModel state in a way that can diverge.\n- One-shot events such as navigation, result codes, file opening, permission requests, or clipboard writes are represented as persistent `UiState` fields that can replay incorrectly.\n- MVI `FeatureIntent` user actions are confused with Android `Intent` launch/extras.\n- Domain/usecase boundaries are bypassed in new screens when a meaningful business action exists.\n- A migration introduces new domain abstractions for a single trivial UI action without reducing real complexity.\n\n## UDF and State Checks\n\nFlag issues when:\n\n- `Screen` functions own business state instead of receiving `state` and callbacks.\n- `remember` / `rememberSaveable` stores source-of-truth data that should survive process or route recreation through ViewModel state.\n- User-driven transient state that should survive recreation, such as a pending delete-confirmation target, is held with plain `remember` instead of `rememberSaveable` or ViewModel state.\n- `LaunchedEffect` keys are unstable or cause repeated data loading, duplicate navigation, duplicate toasts, or repeated service calls.\n- A long-lived `LaunchedEffect` collector captures parent callbacks or context-dependent values that can change without using `rememberUpdatedState`, unless the effect is intentionally keyed to restart.\n- Flows are collected without lifecycle awareness in UI routes where `collectAsStateWithLifecycle()` should be used.\n- List items lack stable keys where mutation, selection, or animation can make state attach to the wrong row.\n- Heterogeneous lazy lists or grids provide `key` but omit `contentType`, reducing composition reuse quality when headers, rows, ads, loading items, or expanded content mix in the same lazy layout.\n- Selection, search query, sorting, filtering, loading, and error states are not represented in a single coherent `UiState`.\n- Derived values are recomputed expensively on every recomposition instead of living in ViewModel state or `remember(key)`.\n- UI effects write list-derived consistency back into the ViewModel, such as pruning selection from `LaunchedEffect(uiState.items)`. Prefer deriving this in the ViewModel with `combine(...)` or reducing it when the data flow updates.\n- State is hoisted higher than the lowest common owner, such as a top-level route collecting a child ViewModel flow only needed inside a conditional branch or one popup menu.\n\n## Compose Stability and Collection Checks\n\nFlag issues when:\n\n- Collection-heavy `UiState` exposed to Compose uses ordinary Kotlin `List`, `Set`, or `Map` in hot paths. Kotlin 2.x strong skipping is helpful but does not make standard Kotlin collections stable to Compose.\n- DAO or repository lists are passed directly through `UiState` to deep composables without conversion to `ImmutableList` / `ImmutableSet` / `ImmutableMap` or a stable UI model wrapper.\n- Review recommendations imply replacing every internal collection. Keep the finding scoped to Compose-facing render state; temporary accumulators, sorting inputs, Room DAO signatures, and domain APIs can remain normal collections unless they are the actual recomposition boundary.\n- Mutable collections such as `ArrayList`, `MutableList`, or mutable maps are stored in Compose state or ViewModel `UiState`.\n- Entity classes from non-Compose modules are passed deeply through composables and cause visible recomposition churn; prefer stable UI render models if measurement or code shape shows this matters.\n\n## Navigation and Compatibility Checks\n\nFlag issues when:\n\n- New Compose destinations skip `MainActivity` Navigation3 route registration.\n- Legacy Android `Intent` extras or result codes change without an explicit compatibility plan.\n- A migrated screen is reachable both through `MainActivity` and a retained Activity with inconsistent state initialization.\n- Navigation is performed directly inside nested composables instead of through callbacks/effects.\n- Activity Result launchers, file pickers, permission requests, or Android DialogFragments are hidden inside reusable UI composables.\n- Back behavior bypasses ViewModel decisions when unsaved changes, selection mode, add-to-shelf prompts, or confirmation dialogs exist.\n\n## UI and Project Convention Checks\n\nFlag issues when:\n\n- Raw Material components ignore `LegadoTheme` or existing shared components where the project already has a wrapper.\n- A screen recreates shared UI already present under `ui/widget/components`.\n- User-facing text is hardcoded instead of using string resources, except for temporary debug-only text.\n- Compose UI keeps ViewBinding, adapter, or XML assumptions after migration.\n- Dialogs and bottom sheets use inconsistent project components when `AppAlertDialog`, `AppModalBottomSheet`, or existing option sheets fit.\n- Image loading bypasses existing Coil cover/image helpers where cover behavior, cache, SVG, or GIF handling matters.\n\n## Clean Architecture Checks\n\nFor new screens, expect:\n\n- Compose renders state and emits user actions.\n- ViewModel owns state, intent handling, and effect emission.\n- Usecases own reusable business actions.\n- Repositories mediate data access.\n- DAOs remain behind repositories unless adding a boundary would be disproportionate and local project patterns already allow direct use.\n\nFor migrated screens, allow pragmatic intermediate code only when:\n\n- It preserves behavior.\n- It is isolated behind a clear compatibility boundary.\n- It does not spread View-era assumptions into new Compose-first code.\n\n## Output Template\n\nUse this shape unless the user asks for another format:\n\n```text\nFindings\n- [P1] Title\n  file:line\n  Impact: ...\n  Fix: ...\n\nOpen Questions\n- ...\n\nNotes\n- No issues found in ... / Tests not run ...\n```\n\nWhen using Codex inline review comments, emit one directive per finding with tight line ranges:\n\n```text\n::code-comment{title=\"[P2] Keep route state in ViewModel\" body=\"...\" file=\"/absolute/path/FeatureScreen.kt\" start=42 end=45 priority=2 confidence=0.8}\n```\n\n## Verification Suggestions\n\nRecommend verification based on the reviewed change:\n\n- Kotlin-only review/fix: `.\\gradlew.bat :app:compileAppDebugKotlin`.\n- Resource/XML/manifest impact: `.\\gradlew.bat :app:assembleAppDebug`.\n- Navigation or compatibility changes: manually open both `MainActivity` route and any retained legacy Android `Intent` entry point.\n- Behavior-heavy ViewModel changes: add or run focused tests only where the project has a practical test seam.\n",".github/copilot-instructions.md":"# Copilot repository instructions\n\nRead and follow `/AGENTS.md` before changing this repository. It is the source of truth for\nproject-wide constraints, source priority, validation, current Android boundaries, and the KMP/CMP\ndirection.\n\nFor specialized work, also read the matching repository skill:\n\n- `/.agents/skills/legado-compose-migration/SKILL.md` for View/XML to Compose or new Android Compose\n  screens.\n- `/.agents/skills/legado-compose-review/SKILL.md` for Compose architecture and behavior review.\n- `/.agents/skills/legado-kmp-migration/SKILL.md` for modularization, `commonMain`, CMP, platform\n  adapters, migration gates, and scaffolding.\n\nDo not infer current package names, Gradle tasks, dependencies, navigation APIs, or platform support\nfrom examples. Confirm them in source, tests, `settings.gradle`, Gradle build files, and\n`gradle/libs.versions.toml`. The KMP target structure in `docs/dev/kmp-cmp-modernization.md` is a\nstaged proposal, not a description of modules that already exist.\n\nKeep changes surgical, preserve existing workspace edits, run risk-matched verification, and report\nexact commands plus unverified behavior.\n","AGENTS.md":"# AGENTS.md\n\n本文件定义仓库级不变量。专项流程放在 `.agents/skills/`，长期方案放在 `docs/dev/`\n；不要把临时任务、讨论过程或一次性结论继续堆进本文件。\n\n## 资料优先级\n\n执行任务前按以下顺序建立上下文：\n\n1. 用户本次目标、范围与验收条件。\n2. 本文件的仓库级约束。\n3. 与任务匹配的 `.agents/skills/*/SKILL.md` 及其明确要求读取的 reference。\n4. `docs/dev/` 中的专项设计或迁移计划。\n5. 当前源码、测试、Gradle 配置和相邻实现；它们用于确认当前版本的真实 API 与行为。\n6. 外部资料只作为设计依据；涉及会变化的 Android、Kotlin、Compose 或库 API 时，以官方当前文档和本仓库版本为准。\n\n文档与源码不一致时，先指出差异。不要为了让实现符合过期文档而静默改代码。\n\n## Skill 路由\n\n- XML/View/Activity/Fragment/RecyclerView 到 Compose 的页面迁移、新 Compose 页面：使用\n  `legado-compose-migration`。\n- Compose Screen、Contract、ViewModel、导航、状态与兼容边界审查：使用 `legado-compose-review`。\n- Gradle 模块拆分、`commonMain` 抽取、KMP/CMP、`expect/actual`、平台能力适配、迁移门禁或相关脚手架：使用\n  `legado-kmp-migration`。\n- 单个任务可组合 skill，但只读取与当前工作切片有关的 reference。\n\n## 工作方式\n\n- 开始前说明假设、成功条件和不在范围内的内容。存在多种会显著改变结果的解释时，不静默选择。\n- 优先做最小、可回滚、可独立验证的垂直切片；不得借任务重构无关代码。\n- 修 bug 时先建立可复现测试或其他可观察证据；重构前后保持同一行为验证通过。\n- 保留工作区中用户已有修改。只移除本次改动产生的无用代码和资源。\n- 不为了“架构完整”创建空模块、单方法包装、无调用方抽象或通用 `Utils/Common` 容器。\n\n## 当前工程事实与目标方向\n\n当前工程是 Android-first 的渐进迁移仓库，不是已经完成的 KMP 工程：\n\n- Gradle 模块：`:app`、`:modules:book`、`:modules:rhino`、`:baselineprofile`；`modules/web` 是独立 Vue 3\n  工程。\n- `:app` 同时包含 Android UI、数据、领域、服务和大量遗留 View 代码；包目录只表达逻辑边界，尚无 Gradle\n  编译隔离。\n- 新 UI 以 Jetpack Compose、Material 3、Navigation 3、Koin、StateFlow/SharedFlow 为默认；阅读器渲染核心等成熟\n  View 代码可作为 Android 专业渲染岛保留。\n- `domain/` 是迁移目标边界，但当前仍存在 Android/JVM 类型渗透；不要把“目标纯净度”描述成已完成事实。\n- 长期目标是渐进演进为 KMP/CMP。详细目标结构、阶段和门禁见 `docs/dev/kmp-cmp-modernization.md`。\n\n### Feature-first 过渡结构\n\n- 新 Compose-first Feature 和完成整页迁移的代码使用 `io.legado.app.feature.<name>`；目录、Gradle\n  path、Kotlin package 分别采用 `feature/book-info`、`:feature:book-info`、`feature.bookinfo`。\n- 在 `:app` 内先形成稳定 Feature 包，再提升为真实 `:feature:<name>` 模块；只有确有多宿主/多模块消费者时才拆\n  `api/impl`，只有确有跨平台价值时才改为 KMP/CMP source sets。\n- Feature 内共置 Contract、ViewModel、Route、Screen 和私有 components/dialog/sheet/model；文件的详细归属与迁移步骤见\n  `docs/dev/feature-first-structure.md`，现有业务 owner 见 `docs/dev/feature-catalog.md`。\n- App host 聚合导航和 DI；Feature 不直接依赖其他 Feature 实现。领域、数据、平台能力不能为了目录整齐塞入\n  Feature UI 包。\n- 旧 `ui/...` 是渐进迁移区，不做全量机械搬家；一个 Feature 的新 owner 建立后，禁止继续向旧包新增同职责文件。\n\n### 目标依赖方向\n\n```text\napp host / composition root\n        ↓\nfeature implementation → feature API\n        ↓\ndomain / model / platform contracts\n        ↓\ndata abstractions\n        ↓\nplatform implementations (Android/JVM/iOS/...)\n```\n\n- 应用宿主负责组装 Feature 实现、DI、平台生命周期和导航运行时。\n- Feature 之间通过稳定契约或导航 API 协作，不直接依赖其他 Feature 的实现。\n- `commonMain` 只容纳经过依赖审计、能被至少一个非 Android 目标编译验证的代码；移动目录不等于完成跨平台迁移。\n- Android `Context`、Room 实体/DAO、Service、Broadcast、Activity Result、Cronet、文件\n  URI、通知、媒体会话、成熟阅读器渲染和现有 Rhino 集成默认留在平台侧，通过窄接口进入共享层。\n- 平台能力不可用时必须显式建模为 capability/unsupported，不得用静默空实现伪造跨平台支持。\n\n## Compose 屏幕约束\n\n- 新屏幕使用 Compose；不要新增 XML/View Activity/Fragment。\n- Android Compose 新屏幕采用 UDF/MVI：`@Stable XxxUiState`、`XxxIntent`、`XxxEffect`；ViewModel 直接继承\n  `ViewModel`，私有 `MutableStateFlow` / `MutableSharedFlow(extraBufferCapacity = 16)`，对外只暴露只读\n  Flow，并由单一 `onIntent` 分发用户动作。共享 Feature 的状态宿主按 KMP skill 单独决策。\n- Screen/Content 保持无业务逻辑、无 DAO/网络/存储直连；ViewModel 通过 Gateway/Repository/UseCase\n  访问业务能力。\n- 所有 UiState 与 UI item 数据类标注 `@Stable`。Compose 渲染边界中的集合使用\n  `kotlinx.collections.immutable`；内部计算和数据层不机械替换集合类型。\n- 导航、权限、文件选择和其他宿主动作通过回调或 Effect 处理。新目的地优先由 `MainActivity` 的 Navigation\n  3 图组装。\n- 使用项目的 `AppScaffold`、主题、top bar、dialog/sheet、列表和设置组件。正确处理 edge-to-edge 与\n  predictive back。\n- Activity 仅在遗留 `Intent`/result 兼容确有需要时作为薄宿主保留。\n\n具体文件形态和审查清单由 Compose migration/review skill 维护，本文件不复制完整模板。\n\n## 数据与设置边界\n\n- UI（包括 ViewModel）不得新增 DAO、`appDb.*Dao`、网络客户端或旧偏好 API 直连；历史债务由\n  `verifyConfigArchitecture` 的基线棘轮冻结并逐步下调。\n- 普通设置 Gateway 通过 `update { current -> current.copy(...) }` 修改状态；关联字段在一次\n  `copy(...)` 中原子提交。\n- 不引入 `*SettingsUpdate` 分发类型或 `updateAll`。\n- `ReadStyleMutation`、`ThemePackageSettingsGateway.applyAndAwait`、`ThemeStateTransaction`、\n  `AppUiConfigurationGateway` 等专用 API 保持现有形态。\n- Koin 中 Gateway 接口到 Repository 实现保持显式绑定；不要用构造函数绑定掩盖接口归属。普通\n  Repository/UseCase/ViewModel 可继续使用项目既有 `singleOf` / `viewModelOf` 约定。\n- 新的共享领域契约不得暴露 `Context`、`File`、`Uri`、Room 类型、Android resource id 或 JVM-only\n  stream；用领域值、`ByteArray`/抽象 source-sink、序列化模型或平台接口表达。\n\n## KMP/CMP 迁移纪律\n\n- 先写行为/契约测试和依赖清单，再移动代码；一个 PR 只完成一个可说明的边界变化。\n- 优先抽取稳定模型、纯函数、规则解析契约和 UseCase；网络、数据库、JS 引擎、服务、阅读器渲染最后通过平台适配器处理。\n- `expect/actual` 只用于真正的平台原语。可用普通接口 + DI 表达的能力，不使用 `expect/actual`。\n- 平台实现由 Koin 和应用 composition root 组装。\n- 不因 KMP 目标同时替换数据库、网络、DI、导航和 UI 框架。每次只改变一个主要风险维度。\n- Feature `api/impl` 只有形成真实 Gradle 边界并存在跨 Feature/宿主调用时才创建，不能只建同名文件夹。\n- 首个非 Android 目标是架构证明，不承诺功能齐平；目标顺序由 capability matrix 和产品需求决定。\n\n## 代码生成\n\n- 生成器用于编码已稳定的约定，不用于发明架构。至少有两个手工迁移且通过审查的同类样本后，才能固化模板。\n- 脚手架必须可预览或 dry-run、拒绝覆盖已有文件、输出最小文件集，并在已有 Feature 上采用追加/AST\n  感知修改，禁止覆盖 route graph 或 DI 文件。\n- 生成的可提交源码仍接受普通代码审查；真正的编译期生成物放入 `build/generated`，不得手工编辑或提交。\n- 模板和生成器变更必须有 fixture/snapshot 或编译验证，并与 convention plugin 的模块类型保持一致。\n\n## 重构审查门禁\n\n每个迁移 PR 至少回答：\n\n- 行为基线是什么，哪些测试或手工路径证明没有回退？\n- 依赖边界净变化是什么，是否减少了 Android/JVM 泄漏或历史基线？\n- 新模块/接口是否有真实调用方，能否用更小改动完成？\n- Android 实现与共享契约的错误、线程、取消、序列化语义是否一致？\n- 做了哪些验证，哪些真机、性能、外部服务或其他平台行为尚未验证？\n- CI 是否执行了受影响的共享测试、目标编译或 adapter contract test？\n\n减少历史违规时必须同步下调对应 baseline；门禁不接受“先放宽基线再迁移”。详细分级见 KMP/CMP 现代化计划。\n\n## 构建与验证\n\n开发与 CI 使用 JDK 21。按风险选择最小充分验证：\n\n```powershell\n# Kotlin/Compose 快速编译\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# 当前主验证集\n.\\gradlew.bat testAppDebugUnitTest lintAppDebug verifyConfigArchitecture assembleAppDebug --continue --no-configuration-cache\n\n# 资源、Manifest、生成绑定或打包变化\n.\\gradlew.bat :app:assembleAppDebug\n\n# Release/R8 相关\n.\\gradlew.bat assembleAppRelease\n.\\gradlew.bat assembleAppNoR8\n\n# Web 前端\npnpm --dir modules/web build\n```\n\nKMP 任务名在模块实际创建后才存在；不要假装运行尚未定义的 `commonTest`、目标编译或 API\n检查任务。新增模块时把其真实任务加入 CI，并在交付中列出准确命令。\n\n所有文本改动至少运行 `git diff --check`。构建通过不替代架构边界、行为和真机性能复核。\n\n## 重要项目约束\n\n- 不将 jsoup 升级到 1.16.2 以上；新版行为会影响 `AnalyzeByJSoup.kt` 与 JsoupXpath。\n- 应用代码仅在两处依赖 Hutool：①作为书源 JS 的运行时加密库保留在 classpath（书源直接调用\n  `Packages.cn.hutool.*`）；②`help/crypto/CryptoUtils.kt` 的 base64 **解码**统一走\n  `cn.hutool.core.codec.Base64.decode`，以兼容缺 `=` 填充等宽松输入（Kotlin\n  `kotlin.io.encoding.Base64` 严格填充会导致部分书源取章名/正文回归）。版本固定 5.8.22 勿升级。\n  应用内部加密主体仍用现有 JCA（`javax.crypto`/`java.security`）与 `help/crypto` 路径，KMP 抽取时\n  再通过能力契约替换 JVM API。\n- 代码 namespace 为 `io.legado.app`，Android `applicationId` 为 `io.legato.kazusa`，不要混用。\n- 当前 minSdk 26、target/compile SDK 37；Release 启用 R8 与资源压缩，`noR8` 变体用于排障。\n- Rhino 书源/RSS/TTS 规则、Android 服务、Web 服务和阅读器渲染属于高行为风险平台能力，迁移前必须建立兼容测试或\n  capability 边界。\n- `modules/web` 必须连接应用内 Ktor WebService；开发环境通过 `VITE_API` 指向设备服务地址。\n\n## 交付说明\n\n最终说明应包含：修改的职责范围、关键设计取舍、实际运行的验证与结果、未验证风险。不要把“已生成代码”或“已移动文件”当作完成标准。\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis is a Material Design 3 fork of [Legado](https://github.com/gedoor/legado). `app/src/main/java/io/legado/app/` uses **Clean Architecture** with three layers:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `:app`, `:modules:book` (epub/TXT parsing, namespace `me.ag2s`), `:modules:rhino` (Rhino JS wrapper, namespace `com.script`). There is also a Vue 3 web frontend in `modules/web/` (pnpm, separate from the Android build).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`MainActivity` holds a single `NavDisplay` with `entryProvider { ... }` defining all composable entries. `Launcher0` through `LauncherW` extend `MainActivity` to provide multiple launcher icon alias entries. Separate activities handle the reader (`ReadBookActivity` — still View-based), book info, source management, replace rules, file manager, QR scanner, etc.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 theme modes (`AppThemeMode` enum) — Dynamic (Monet), 12 named presets, Custom (MaterialKolor seed-color generation), Transparent. `CustomColorScheme` wraps `com.materialkolor` with configurable `PaletteStyle` (TonalSpot, Neutral, Vibrant, Expressive, Rainbow, etc.) and `ColorSpec` (2021 vs 2025).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.claude/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook sources, RSS sources, and HTTP TTS use JavaScript rules. `initRhino()` in `App.kt` registers `NativeBaseSource` wrappers for `BookSource`, `RssSource`, `HttpTTS` (writable JS objects) and `ReadOnlyJavaObject` wrappers for rule entities. Rule parsing logic lives in `help/source/` and `model/analyzeRule/`.\n\n## Important Constraints\n\n- **Do not update jsoup** beyond 1.16.2 — a breaking change in newer versions (see [jsoup#2017](https://github.com/jhy/jsoup/pull/2017)) affects `AnalyzeByJSoup.kt` and the JsoupXpath library\n- Hutool is back on the classpath at 5.8.22 (do not upgrade). Book-source JS calls it via\n  `Packages.cn.hutool.*`; app-internal **base64 decoding** in `help/crypto/CryptoUtils.kt` also\n  routes through `cn.hutool.core.codec.Base64.decode` for lenient input compatibility (Kotlin\n  `kotlin.io.encoding.Base64` is strict about `=` padding). App crypto otherwise uses JCA (\n  `javax.crypto`/`java.security`); new internal tools live in `help/crypto/CryptoUtils.kt`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated in `modules/web/` — a Vue 3 + TypeScript + Vite project for remote bookshelf and source editing. Must connect to the app's built-in HTTP server (started via `WebService` in the main activity settings). Commands:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n"},"files":{".agents/skills/legado-compose-migration/SKILL.md":"---\nname: legado-compose-migration\ndescription: Create or migrate a Legado Android screen with Jetpack Compose while preserving behavior and following the repository's Feature ownership, UDF, navigation, DI, inset, and compatibility boundaries. Use for XML/View/RecyclerView/DialogFragment migrations and new Android Compose destinations. Use legado-compose-review instead for review-only requests, and legado-kmp-migration when the change crosses a multiplatform or Gradle boundary.\n---\n\n# Legado Compose Migration\n\n## Purpose\n\nDeliver one working Android UI surface without importing View-era ownership into new Compose code.\nFor migrations, preserve observable behavior before cleanup. For new screens, use the repository's\ncurrent Feature-first and UDF conventions.\n\nBefore editing, read `AGENTS.md`, the relevant part of `docs/dev/feature-first-structure.md`, the\ncurrent implementation and one or two nearby examples that exercise the same behavior. Read\n[references/project-patterns.md](references/project-patterns.md) for repository-specific placement,\nstate and host details; examples are evidence, not templates to copy blindly.\n\n## Decide the shape\n\n- **New destination:** prefer a `MainActivity` Navigation 3 destination. Do not create a standalone\n  Activity without an external-entry or platform lifecycle reason.\n- **Migrated destination:** retain an Activity only as a thin compatibility host when existing\n  callers require stable Android Intent extras/results or a framework contract.\n- **Partial migration:** keep mature View rendering or platform integrations as explicit islands\n  when replacing them would add behavioral risk unrelated to the request.\n- **Multiplatform boundary:** stop treating this as only an Android screen migration and also use\n  `legado-kmp-migration`.\n\n## Workflow\n\n1. Bound the surface and success criteria. Record inputs, navigation/results, empty/loading/error\n   states, actions, dialogs/sheets, persistence, back behavior and external effects that must\n   remain.\n2. Read the smallest complete slice: UI, state owner, host/route, DI, resources, data/use-case\n   calls,\n   and any old implementation needed as a behavior reference.\n3. Choose the lowest state owner that reads and writes each value. Put durable business/render state\n   in `UiState`; keep purely local focus, expansion, animation and draft state in the UI when no\n   higher owner needs it. Route Feature/business actions through the Feature intent type. A direct\n   user action that only requests host navigation may use a host callback. Dialog/sheet visibility\n   is UI state; Android framework dialogs, permissions, file pickers and launches stay in the host.\n4. Keep Screen/Content free of repository, DAO, network, storage and service access. New\n   presentation\n   code uses Repository/Gateway/UseCase boundaries. Preserve an existing violation only when the\n   task is explicitly UI-only, do not spread it, and record the remaining debt.\n5. Wire navigation, DI and compatibility behavior at the host boundary. Keep public extras, result\n   codes and deep-link semantics stable unless the user requested an API change.\n6. Delete XML, adapters, bindings and resources only after references and compatibility callers are\n   gone.\n7. Verify in proportion to risk and report both passed evidence and unverified manual/device paths.\n\n## Project constraints\n\n- New ownership lives under `io.legado.app.feature.<name>`; legacy `ui/...` is a migration source,\n  not the destination for a second implementation.\n- Use ViewModel-owned read-only `StateFlow` and one `onIntent` dispatcher for Feature/business\n  actions, as required by `AGENTS.md`. When a Feature has best-effort transient effects, expose the\n  project's `SharedFlow(extraBufferCapacity = 16)` shape. Buffer capacity does not make emissions\n  durable while no UI is collecting: navigation/result/destructive completion and other\n  consistency-critical ViewModel outcomes must reduce to state or use an explicit acknowledgement\n  protocol instead of relying on an effect stream.\n- Annotate Compose-facing `UiState` and UI item models with `@Stable` only while honoring the\n  annotation's contract: public mutable properties and unobservable mutation make it incorrect.\n  Use immutable collections at the rendering boundary; do not change repository/data collections\n  mechanically.\n- On Android, collect `Flow` UI state with `collectAsStateWithLifecycle` unless a documented\n  lifecycle requirement calls for something else. A `LaunchedEffect` collector is scoped to the\n  composition, not automatically to `STARTED`/`RESUMED`. Long-lived effects must use appropriate\n  keys and `rememberUpdatedState` for changing values that should not restart them.\n- Reuse project theme and components when their behavior fits. A wrapper's existence is not enough\n  reason to use it when semantics, accessibility or host requirements differ.\n- Insets are an ownership decision. Inspect `Scaffold` `contentWindowInsets`, whether content\n  consumes `innerPadding`, nested scaffolds/sheets and IME behavior. Do not assume that using\n  `Scaffold` alone proves edge-to-edge correctness, and do not double-apply system-bar padding.\n- Preserve predictive-back behavior. Use ordinary `BackHandler` only for binary interception; use\n  `PredictiveBackHandler` when custom UI needs gesture progress. If Navigation 3 transitions are\n  customized, define and verify predictive-pop behavior as well as the normal pop transition.\n- The app targets API 37, where orientation, aspect-ratio and resizability restrictions no longer\n  protect layouts on large screens. New or substantially migrated destinations must tolerate\n  compact, medium and expanded resizable windows, rotation and recreation. Base layout decisions on\n  the current app window, not physical-device assumptions; do not add an adaptive library or\n  multi-pane layout unless the screen's behavior benefits from it.\n- Prefer Material/Foundation interactions for their semantics, focus and keyboard behavior. Custom\n  controls and gestures must provide appropriate role/state/action semantics, keyboard/D-pad access\n  and an adequate touch target.\n- Keep localized user-facing text in resources and retain fallback behavior.\n\n## Verification\n\n- Kotlin-only presentation/host changes: run `:app:compileAppDebugKotlin` using the repository's\n  Gradle wrapper syntax for the current shell.\n- Resource, manifest, XML/binding deletion or packaging changes: run `:app:assembleAppDebug`.\n- State/business changes: run focused ViewModel/use-case tests or add a characterization seam.\n- Navigation/Intent/results/insets/back: manually or instrumentedly exercise every retained entry\n  path that compilation cannot prove.\n- New or substantially changed layouts: exercise representative compact and expanded resizable\n  windows, rotation/recreation and IME interaction rather than validating only one portrait phone.\n- Always run `git diff --check`.\n\nUse actual task names from this checkout; do not invent tasks from this skill.\n",".agents/skills/legado-compose-migration/references/project-patterns.md":"# Legado Android Compose Project Patterns\n\nRead this reference while implementing a screen. Confirm every referenced API against the current\ncheckout because the repository is mid-migration.\n\n## Ownership and placement\n\n- Android application module: `:app`.\n- Canonical new Feature package: `io.legado.app.feature.<name>`.\n- `ui/...` is legacy/migration territory. Keep only explicit compatibility owners there after a\n  Feature establishes its canonical package.\n- `MainActivity` owns new in-app Navigation 3 destinations and the root graph.\n- A retained Activity translates stable external/legacy Intent inputs and results; it is not a\n  second presentation or navigation owner.\n- App-level Koin aggregation remains in the host. Use the repository's existing `viewModelOf` or\n  parameterized `viewModel` convention rather than adding a second DI pattern.\n\nA Feature may contain Contract, ViewModel, Route, Screen, components, dialogs, sheets and\npresentation models, but create only the files its behavior needs. Directory shape is not an\nacceptance criterion.\n\n## State and effects\n\nFor a behavior-heavy new screen, the repository expects:\n\n- `@Stable` Compose-facing `UiState` and UI item models;\n- immutable collections at the Compose rendering boundary;\n- private `MutableStateFlow`, exposed as read-only `StateFlow`;\n- when best-effort transient effects are actually needed, private\n  `MutableSharedFlow(extraBufferCapacity = 16)`, exposed as read-only `SharedFlow`;\n- one `onIntent` dispatcher for user actions;\n- host actions expressed as effects or callbacks.\n\n`@Stable` is an assertion, not a magic optimization. Every public property must remain stable and\nchanges observed by Compose. Wrap unstable data only when the wrapper has correct equality and\nmutation semantics. Use measurement/compiler reports before inventing performance abstractions.\n\nKeep source-of-truth business state in the ViewModel. Local `remember`/`rememberSaveable` is\nsuitable\nfor UI affordances and restorable drafts/IDs, not a second copy of repository state. Avoid\nUI-to-ViewModel feedback loops; restore consistency in the reducer or flow that produces the state.\n\nClassify host actions by delivery semantics:\n\n- A user click whose only meaning is UI navigation can call a host callback directly; use a\n  lifecycle-aware rapid-click guard such as `dropUnlessResumed` where the available Lifecycle\n  version supports it.\n- A ViewModel outcome that must survive a missing collector becomes state with an acknowledgement\n  or another explicit durable protocol.\n- Snackbar/toast/haptic feedback that is intentionally best-effort may use the Feature effect\n  stream. Document the loss behavior rather than assuming `extraBufferCapacity` solves it.\n\n## Host boundaries\n\nKeep these at the route/host unless the project already has a tested abstraction:\n\n- navigation and result delivery;\n- permission and Activity Result launchers;\n- file/document pickers;\n- Android framework dialogs and services;\n- Context-dependent clipboard, URI and external-app operations.\n\nReusable Screen functions receive state and semantic callbacks. They do not know an Activity,\nbinding, DAO, application singleton or root back stack.\n\n## Lists, lifecycle and recomposition\n\n- Use stable keys when item identity survives insertion, removal or reorder; use `contentType` when\n  heterogeneous reuse matters.\n- Collect Android route state with `collectAsStateWithLifecycle` unless another lifecycle is\n  deliberate and documented. Apply the same delivery analysis to effect collectors; composition\n  lifetime and host `RESUMED` state are not interchangeable.\n- Key `LaunchedEffect` by the lifetime it represents. Use `rememberUpdatedState` for changing\n  callbacks captured by an effect that should not restart.\n- Derive cheap display values in composition; memoize or move work only when its cost/lifetime\n  warrants it.\n- Prefer immutable Compose-facing collections, while leaving temporary computations and data-layer\n  APIs in their natural collection types.\n- Strong Skipping is enabled by default on modern Kotlin/Compose compiler versions. Unstable\n  parameters are compared by identity, so avoid needless instance churn, but do not add wrappers or\n  `@Stable` solely from intuition. The repository requires the annotation on UI state/item types;\n  those types must actually satisfy its equality and observable-mutation contract.\n\n## Insets and back\n\nTrace which layer owns each inset. For a Material 3 `Scaffold`, inspect its configured\n`contentWindowInsets` and verify that content applies/consumes the provided padding correctly.\nSheets, dialogs, IME and nested scaffolds may need separate treatment. Visual/manual evidence is\nrequired; the presence of `Scaffold`, `safeDrawing` or padding modifiers alone proves nothing.\n\nNavigation 3 integration does not remove the need to verify custom back interception, selection\nmode, unsaved changes and retained Activity entry points. Route back actions through the same owner\nthat decides whether leaving is allowed. Use `PredictiveBackHandler` when gesture progress drives\nUI;\nwhen customizing `NavDisplay` transitions, also supply/test predictive-pop behavior.\n\n## Adaptive layouts and accessibility\n\nBecause this app targets API 37, large-screen orientation, aspect-ratio and resizability\nrestrictions\ncannot be used as a compatibility fallback. Treat the current app window as dynamic across rotation,\nfold/unfold, split-screen and desktop windowing.\n\n- Test compact and expanded widths for new or substantially migrated destinations.\n- Use current window metrics/window size classes for layout decisions; do not branch on a physical\n  device category or assume portrait.\n- Preserve important input/draft/selection state across recreation with the appropriate local\n  saveable state or `SavedStateHandle`, based on the state owner and size.\n- Do not stretch a phone layout indefinitely. Adopt list-detail/supporting panes or adaptive\n  navigation only when the Feature benefits; avoid adding a library merely to satisfy a checklist.\n- Prefer standard interactive components/modifiers. Custom pointer input needs semantic actions,\n  focus/keyboard access and a usable touch target.\n\n## Architecture boundary\n\nNew UI and ViewModels do not add DAO, `appDb`, network-client or old preference access. Use existing\nGateway/Repository/UseCase contracts, or add the smallest real boundary with an actual caller.\n\nFor a strictly UI-only migration, an existing presentation-layer violation may remain to avoid\ncombining architecture and UI rewrites. Freeze rather than duplicate it, document it, and do not\ndescribe the screen as fully modernized until the boundary is corrected.\n\n## Verification selection\n\n- Kotlin-only: `:app:compileAppDebugKotlin`.\n- Resources/manifest/XML/generated binding/package: `:app:assembleAppDebug`.\n- Changed state transitions: focused unit tests with success/failure/cancellation cases as relevant.\n- Navigation or compatibility: exercise the MainActivity route and every retained Intent/result\n  entry.\n- Insets, IME, accessibility and predictive back: device/emulator evidence where risk warrants it.\n- New/substantially changed destinations: representative compact and expanded window checks plus\n  rotation/recreation; add targeted adaptive UI tests when layout branching is meaningful.\n\nUse `AGENTS.md` for the canonical full verification set and exact wrapper command.\n\n## Current official references\n\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting)\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle)\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events)\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n- [Compose side effects](https://developer.android.com/develop/ui/compose/side-effects)\n- [Material Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n- [Adaptive layouts for resizable apps](https://developer.android.com/develop/adaptive-apps/guides/app-orientation-aspect-ratio-resizability)\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n",".agents/skills/legado-compose-review/SKILL.md":"---\nname: legado-compose-review\ndescription: Review existing Legado Android Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, and compatibility hosts for concrete behavior, lifecycle, state ownership, architecture, inset, accessibility, and maintainability risks. Use for review or audit requests. Do not rewrite code unless fixes are requested; use legado-compose-migration for implementation and legado-kmp-migration for multiplatform or Gradle-boundary findings.\n---\n\n# Legado Compose Review\n\n## Purpose\n\nFind concrete defects and risky drift before proposing rewrites. Judge the code against current\nrepository behavior and constraints, not a preferred template.\n\nRead `AGENTS.md`, the scoped files and\n[references/review-checklist.md](references/review-checklist.md). Consult an old View implementation\nonly when it remains a caller or behavior baseline.\n\n## Review workflow\n\n1. Define the exact surface and whether the user asked for review only or fixes. Review-only work\n   does not authorize edits.\n2. Inspect the smallest complete behavior slice: Screen, state owner/contract, route/host, DI,\n   relevant repositories/use cases, resources and compatibility entry points.\n3. Trace state and effects end to end. Look for divergent owners, replayed one-shot work, lifecycle\n   mistakes, lost events, unstable identity and error/cancellation paths before style issues.\n4. Trace navigation/results, Insets/IME, back, accessibility and platform launchers through the real\n   host. Do not infer correctness from the presence of a particular API.\n5. Report findings first with tight line references, concrete impact and the smallest credible fix.\n   Separate compatibility-preserving fixes from optional redesign.\n6. If no finding exists, say so and list the important unverified runtime/device paths.\n\n## Severity\n\n- **P0/P1:** crash, data/behavior loss, broken navigation/result/entry compatibility, unsafe\n  lifecycle or concurrency, repeated destructive effect, inaccessible/obscured critical content.\n- **P2:** state ownership that can diverge, business/data access in UI, platform leakage, incorrect\n  stability contract, recurring recomposition/performance issue, or architecture drift with a real\n  maintenance cost.\n- **P3:** localized convention, naming, testability or cleanup issue without demonstrated behavior\n  or structural impact.\n\nDo not promote a missing annotation, different file split or wrapper choice to P2 without explaining\nthe observable or architectural consequence.\n\n## Boundaries\n\n- Do not require every screen to have the same number of files or abstractions.\n- Allow thin compatibility Activities and mature View/platform islands when they have a current\n  caller and one clear owner.\n- New Compose presentation must not add DAO, network, storage or service access; report existing\n  debt without turning a review into an unauthorized domain rewrite.\n- Treat `@Stable` as a contract that can itself be wrong, not as proof of stability or performance.\n- Strong Skipping is enabled by default with modern Kotlin/Compose compiler versions, so an\n  unstable parameter is not automatically a performance defect. Check identity churn, actual\n  recomposition evidence and compiler/benchmark data. A missing project-required annotation is P3\n  unless it causes a concrete architectural or runtime issue; an incorrect `@Stable` promise can be\n  P2 because it can suppress required updates.\n- Android `Flow` state should normally be collected with `collectAsStateWithLifecycle`. A\n  composition-scoped collector is not automatically lifecycle-aware merely because it uses\n  `LaunchedEffect`.\n- Review each ViewModel-originated `SharedFlow` effect by delivery semantics. Buffer capacity does\n  not make an event durable while no UI is collecting. Navigation, result delivery, payments,\n  destructive completion and other consistency-critical outcomes should normally reduce to state\n  or use an explicit acknowledgement protocol; best-effort transient feedback may remain an effect.\n- UI-only behavior should stay at the lowest UI owner. A direct user navigation callback handled by\n  the host is valid and often preferable to a ViewModel round trip; guard rapid/replayed navigation\n  with lifecycle-aware mechanisms such as `dropUnlessResumed` when the available Lifecycle version\n  supports it.\n- Insets and predictive back require ownership and runtime-path analysis; `Scaffold`, padding APIs,\n  Navigation 3 or `BackHandler` alone do not prove correctness.\n- Use `PredictiveBackHandler` when custom UI needs gesture progress; use ordinary `BackHandler` only\n  when binary interception is sufficient. When Navigation 3 transitions are customized, inspect\n  `predictivePopTransitionSpec` as well as normal pop behavior.\n- Reusing a project component is preferred only when its behavior, semantics and accessibility fit.\n- Prefer Material/Foundation interaction APIs because they provide semantics, focus and input\n  behavior. Custom gestures/components must preserve role, state/action semantics, keyboard/D-pad\n  access and an adequate touch target.\n\n## Current official baseline\n\nWhen library behavior matters, verify it against the repository version and current official docs:\n\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n  and [stability contracts](https://developer.android.com/develop/ui/compose/lifecycle).\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle).\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting).\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events).\n- [Material 3 Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n  and [Compose Insets](https://developer.android.com/develop/ui/compose/system/insets-ui).\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n  and [Navigation 3 transitions](https://developer.android.com/guide/navigation/navigation-3/animate-destinations).\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n  and [semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics).\n\nWhen implementation is requested, also read the migration skill's project patterns.\n",".agents/skills/legado-compose-review/references/review-checklist.md":"# Legado Compose Review Checklist\n\nUse only the sections relevant to the scoped surface.\n\n## Context\n\n- Confirm the canonical Feature owner and whether any legacy `ui/...` file is a compatibility owner.\n- Read Screen/Content, ViewModel/state contract, route/Activity, DI and directly used domain/data\n  contracts.\n- Read old XML/View/adapters only when needed for parity or a live caller.\n- Identify all entry points: MainActivity route, Intent/deep link, notification/widget/service or\n  Activity Result.\n\n## Behavior and state\n\n- Is there one durable state owner, or can ViewModel, Activity, `remember`, adapter and repository\n  copies diverge?\n- Is each state hoisted only to the lowest owner that needs to read and write it? Local expansion,\n  focus or animation state need not be forced into a ViewModel; business state must not be copied\n  into local Compose state.\n- Can loading, empty, error, selection, query, sorting and dialog/sheet states be reconstructed\n  after\n  recreation where required?\n- Are one-shot effects consumed once without becoming persistent replayable flags?\n- Can a ViewModel-produced effect be emitted while the route has no active collector? If losing it\n  would leave navigation, results or durable business state inconsistent, model/acknowledge it as\n  state rather than relying on `SharedFlow` buffering.\n- Are success, failure, cancellation and stale-response ordering handled?\n- Do `LaunchedEffect` keys match the intended lifetime and avoid duplicate loads/navigation/toasts?\n- Is `Flow`-backed Android UI state collected with `collectAsStateWithLifecycle`, or is there a\n  documented lifecycle reason for another collector?\n- Are mutable collections/entities exposed in a way that invalidates a claimed `@Stable` contract?\n- With Strong Skipping, are unstable parameters recreated on each update and compared by identity?\n  Ask for compiler/measurement evidence before treating ordinary recomposition as a defect.\n- Do lazy items use stable identity where insert/remove/reorder can attach state to the wrong row?\n\n## Architecture and ownership\n\n- Screen/Content renders state and emits semantic actions; it does not access DAO, repository,\n  network, storage, service or global application state.\n- New ViewModel code uses Gateway/Repository/UseCase boundaries and does not add DAO or old global\n  access.\n- Host owns navigation, framework launchers, permissions, Context/URI work and root lifecycle.\n- A retained Activity translates compatibility inputs/results without becoming a second business or\n  navigation owner.\n- Feature code does not depend on another Feature implementation.\n- New files do not extend a legacy package after a canonical Feature owner exists.\n\n## Navigation and compatibility\n\n- New in-app destinations are registered in the current MainActivity graph unless an external-entry\n  requirement justifies an Activity.\n- Existing extras, result codes, deep links and callers preserve semantics.\n- Multiple entry paths initialize equivalent state and converge on the same owner.\n- Nested UI requests navigation through callbacks/effects rather than mutating the root stack.\n- Pure UI navigation can call a host callback directly. Check rapid taps and lifecycle state; use a\n  lifecycle-aware guard such as `dropUnlessResumed` where supported rather than inventing a\n  ViewModel effect solely to move the navigation call.\n- Back interception covers real selection/unsaved state without breaking predictive back or host\n  result delivery.\n- Custom gesture-progress UI uses `PredictiveBackHandler`; customized Navigation 3 pop transitions\n  also define/verify predictive-pop behavior.\n\n## UI, Insets and accessibility\n\n- Trace `Scaffold` `contentWindowInsets`, inner padding consumption, nested scaffolds/sheets, system\n  bars and IME. Check for both overlap and double padding.\n- User-facing strings retain localization and fallback behavior.\n- Project components/theme are used when their semantics fit; wrappers are not applied mechanically.\n- Touch targets, focus order, semantics/content descriptions and keyboard behavior fit the control.\n- Custom pointer input has an accessible semantic action and keyboard/D-pad alternative; standard\n  Material/Foundation interactions are preferred when their behavior fits.\n- Lists/images preserve existing cache, loading and accessibility behavior where relevant.\n- Removed XML/binding/menu resources have no remaining references.\n\n## Verification and output\n\nTie each finding to evidence and the smallest fix. Recommend only checks that can observe the risk:\n\n- compilation for Kotlin wiring;\n- assemble for resources, manifest, XML/binding and packaging;\n- focused unit tests for reducers/ViewModels/use cases;\n- route plus retained Intent/result path checks for navigation compatibility;\n- device/emulator checks for Insets, IME, accessibility, predictive back and rendering performance.\n\nUse P0/P1/P2/P3 from the skill. If there are no findings, explicitly list what was not executed or\nobserved.\n",".agents/skills/legado-kmp-migration/SKILL.md":"---\nname: legado-kmp-migration\ndescription: Plan, implement, or review Legado KMP-first modularization across shared domain, data, and presentation code with independently chosen Android, Desktop, Windows-native, and optional iOS renderers. Use for Gradle boundaries, commonMain extraction, platform capabilities, native or process bridges, selective CMP, target gates, and legacy-owner removal. Do not use for an Android-only View-to-Compose rewrite unless it also changes a multiplatform or Gradle boundary.\n---\n\n# Legado KMP-first Migration\n\n## Purpose\n\nMove one verified responsibility toward a reusable KMP boundary without weakening Android behavior\nor forcing every host to use Compose. Optimize for shared behavior and explicit platform contracts,\nnot maximum shared UI percentage.\n\nValid renderers include Android Compose/View islands, Compose Desktop with Material or Fluent,\nWindows-native WinUI 3, and optional SwiftUI/UIKit. CMP is one renderer technology, not the\narchitecture root.\n\nBefore acting, read repository `AGENTS.md`, `docs/dev/kmp-cmp-modernization.md`, and the relevant\npart of `docs/dev/feature-first-structure.md`.\n\nRead supporting references only when applicable:\n\n- Implementation or review: [references/slice-checklist.md](references/slice-checklist.md).\n- Multiple renderers, WinUI 3, SwiftUI/UIKit, native export or IPC:\n  [references/renderer-host-strategy.md](references/renderer-host-strategy.md).\n\n## Choose the work mode\n\n- **Architecture/plan:** inventory current dependencies and consumers, then choose the sharing\n  boundary, renderer strategy, targets, gates and rollback point.\n- **Pure KMP extraction:** move stable models, rules, ports, use cases, reducers or stores into\n  Compose-free `commonMain`.\n- **Data/runtime extraction:** separate domain contracts from storage, network, files and rule\n  engines while retaining compatibility semantics under contract tests.\n- **Renderer boundary:** split renderer-neutral presentation from Material/Miuix, Fluent, WinUI 3,\n  SwiftUI/UIKit or specialized reader UI.\n- **Selective CMP:** share a Compose Screen/resource set only when real hosts deliberately choose\n  the same renderer and interaction model.\n- **Native host bridge:** expose a small, versioned API through an Apple framework, Windows DLL/C\n  ABI, or process/IPC boundary; do not export the internal Kotlin object graph.\n- **Build logic:** prove a target/convention/gate change with one representative module before\n  broader rollout.\n- **Review:** report findings and unverified targets first; edit only when fixes or plans were\n  requested.\n\n## Decisions required before implementation\n\n1. **What is shared?**\n    - Default candidates are domain values, business rules, repository ports, use cases,\n      serialization models, state snapshots, commands and reducers.\n    - Screens, design-system components, navigation renderers, platform ViewModel owners, resource\n      handles, lifecycle and OS integration are not shared by default.\n    - A pure presentation API must not expose Compose annotations/types, Material/Fluent types,\n      Android resources, Koin, Room, or platform SDK objects.\n\n2. **Who renders it?**\n    - Name each committed host and renderer separately; “Desktop” is not a renderer.\n    - Different renderers may consume the same presentation contract while owning distinct layouts,\n      resources, navigation and accessibility.\n    - Do not invent universal UI wrappers across unrelated design systems. Share semantic values\n      only\n      when their meaning is genuinely common.\n\n3. **How does a non-Kotlin host consume it?**\n    - JVM Compose hosts can depend on KMP/JVM modules directly.\n    - SwiftUI/UIKit needs a deliberately exported Apple framework facade.\n    - WinUI 3 needs either a Kotlin/Native `mingwX64` DLL with a narrow C ABI or a separately\n      packaged\n      JVM process reached through a versioned local IPC protocol.\n    - Keep rich Kotlin types internal. Define DTO/state snapshots, commands, errors, cancellation,\n      callback threads, allocation ownership and disposal at the bridge.\n\n4. **What evidence supports the claim?**\n    - Distinguish metadata/compile, contract-test, host smoke, package and release-ready evidence.\n    - Android plus JVM compilation does not prove iOS or Windows Native readiness.\n    - Do not document a module, target, task or host as current until it exists in this repository.\n\n## Workflow\n\n1. Bound one slice: exact files, callers, behavior, intended owner/source set, host impact and\n   rollback point.\n2. Classify dependencies as `common-ready`, `contract-needed`, `renderer-specific`,\n   `platform-island`, or `unknown`. Imports are only a first pass; compile every claimed target.\n3. Establish characterization or contract tests before moving behavior. Preserve storage, script\n   ABI, serialization, error, cancellation, ordering and threading semantics.\n4. Choose the smallest seam. Prefer interfaces plus constructor injection; use `expect/actual` only\n   for true platform primitives or a documented case where injection cannot provide the boundary.\n5. Implement additively, migrate a bounded caller set, then delete the old owner when no callers\n   remain. Do not keep internal `Help/Utils/Base/Provider` facades for import compatibility.\n6. Run actual repository gates and target tasks. Never infer task support from the target design.\n7. Report behavior evidence, dependency/baseline delta, capability changes, exact commands,\n   rollback path and unverified hosts/devices.\n\n## Boundary rules\n\n- Pure KMP domain/presentation does not depend on Compose, AndroidX ViewModel, Room entities/DAO,\n  Koin, platform resources, `File`/URI, JVM-only libraries or renderer types.\n- Renderer modules may depend on presentation; presentation never depends on a renderer.\n- Each host owns its composition root, root navigation/window lifecycle, platform effects,\n  implementation selection and packaging.\n- Unsupported capabilities are explicit; a successful no-op is not an implementation.\n- Feature-to-Feature implementation dependencies remain forbidden. Create `api/impl` only for a\n  real Gradle consumer; split renderers by responsibility instead of generic layering names.\n- Do not change storage, network, DI, navigation and UI technology in one slice.\n- Historical baselines only decrease. A new source set starts at zero and cannot justify raising a\n  baseline.\n\n## Verification minimums\n\n- Always retain the affected Android G0 gates.\n- Pure KMP: common tests plus every claimed target compile.\n- Data/runtime: adapter contracts and compatibility checks proportional to storage, serialization,\n  cancellation and error risks.\n- CMP renderer: target compile plus rendered UI/semantics smoke and runtime dependency alignment.\n- WinUI 3 DLL: native link, generated API review, consumer smoke, memory/disposal/error/thread tests\n  and package loading.\n- WinUI 3 IPC: protocol compatibility, startup/shutdown/reconnect, local-access policy,\n  cancellation and installer smoke.\n- iOS native UI: framework export plus a Swift consumer compile/observation/lifecycle smoke.\n- Always run `git diff --check`; use a clean rebuild after cross-module moves.\n\n## Review output\n\nList findings by impact with tight file/line evidence and the smallest credible fix:\n\n- P0/P1: behavior or data loss, ABI/storage incompatibility, broken target, lifecycle/thread/\n  cancellation defect, or package/runtime failure.\n- P2: renderer leakage into presentation, illegal dependency, false capability, untested bridge,\n  state duplication, baseline relaxation, or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf there are no findings, state which host, package, device, renderer, interop and performance paths\nremain unverified.\n",".agents/skills/legado-kmp-migration/agents/openai.yaml":"interface:\n  display_name: \"Legado KMP-first Migration\"\n  short_description: \"Plan shared KMP logic and independent native or Compose renderers\"\n  default_prompt: \"Use $legado-kmp-migration to plan a reviewable KMP-first slice with explicit host, renderer, interop, and verification boundaries.\"\n",".agents/skills/legado-kmp-migration/references/renderer-host-strategy.md":"# Renderer and Host Strategy\n\nRead this reference when a Feature may use more than one UI stack or when a non-Kotlin host consumes\nshared Kotlin behavior.\n\n## Separate three decisions\n\n1. **Shared behavior:** domain, repositories, use cases, state snapshots, commands and reducers.\n2. **Host integration:** Gradle/JVM, Apple framework, Windows DLL/C ABI, or process/IPC.\n3. **Renderer:** Android Material/Miuix, Compose Material/Fluent, WinUI 3, SwiftUI/UIKit, or a\n   specialized platform island.\n\nDo not infer one decision from another. A KMP core does not require CMP, and Compose on one host\ndoes\nnot require every host to share the same Screen.\n\n## Renderer-neutral presentation\n\nPrefer immutable state snapshots, stable IDs, commands/intents, effects, domain values and explicit\ncapability/error states. Keep Composable lambdas, `Modifier`, Material/Fluent types,\n`StringResource`, icons/painters, navigation UI objects, Android Context, Swift/WinRT objects, Room\nentities and DI containers out of the boundary.\n\n`@Stable` belongs to a Compose-facing contract or adapter. If WinUI 3 or SwiftUI also consumes the\nstate, keep the pure state free of renderer annotations and adapt it at the Compose boundary.\n\n## WinUI 3 choices\n\nWinUI 3 cannot consume a Kotlin/JVM Gradle module like a Kotlin host. Choose the bridge explicitly.\n\n### Kotlin/Native DLL and C ABI\n\nUse a `mingwX64` shared library when in-process calls and one packaged process matter, and only when\nthe required dependency closure supports the target.\n\n- Export a small facade, not repositories, Flow, sealed hierarchies or generic Kotlin APIs.\n- Use opaque handles with explicit create/dispose.\n- Define ownership for every returned string or buffer.\n- Map async work to request IDs plus callbacks/polling; define callback thread and cancellation.\n- Version the ABI and compile a native consumer in CI.\n- Do not force JVM-only Room, Rhino or jsoup dependencies into the DLL; split a native-compatible\n  core or choose IPC for those capabilities.\n\n### JVM sidecar and IPC\n\nKeep the existing JVM data/runtime stack in a separately packaged process and expose a versioned\nlocal protocol when reuse is more important than in-process integration.\n\n- Define protocol DTOs and version negotiation.\n- Define startup, readiness, shutdown, crash recovery and upgrade behavior.\n- Restrict local access; do not unintentionally expose an unauthenticated network service.\n- Propagate cancellation and structured errors.\n- Package and test both processes as one product.\n\nUse one bounded read/write Feature to compare cold start, call latency, state mapping, database and\nruntime reuse, memory ownership, crash isolation, installer complexity and test ergonomics before\nchoosing the product-wide bridge.\n\n## iOS native renderer\n\nExport a deliberately small Apple framework facade. SwiftUI may observe a tested shared state host\nor own an `ObservableObject` that calls shared repositories/use cases. Verify Swift names,\noptionality, collections, async/Flow bridging, lifecycle, cancellation and memory ownership with a\nSwift consumer test.\n\n## Selective CMP\n\nShare a CMP Screen only when real hosts intentionally share its visual and interaction model.\nResources and design-system components belong to that renderer. If Desktop uses WinUI 3 and iOS\nuses SwiftUI, Android Compose can remain an Android renderer even when its code is technically\nportable.\n",".agents/skills/legado-kmp-migration/references/slice-checklist.md":"# KMP Slice Checklist\n\nUse this reference for implementation plans, extraction work, scaffolding or reviews. The default\npath is renderer-neutral KMP. CMP checks apply only after a shared Compose renderer has been chosen;\nthey are not instructions to convert every Feature or host to Compose.\n\n## Before change\n\n- Scope names exact files/packages, current callers, one boundary and one rollback point.\n- Existing behavior is captured by tests or a written parity list.\n- The target owner, source set and consumers are real rather than copied from the target diagram.\n- Dependencies are classified as common-ready, contract-needed, renderer-specific,\n  platform-island or unknown.\n- Actual Gradle target and verification task names have been inspected.\n\n## Contract quality\n\n- Public types express domain values instead of Android/JVM/storage/rendering details.\n- Error, cancellation, threading, transaction, ordering, serialization and ownership semantics are\n  explicit where they cross the boundary.\n- Capability absence is visible to callers.\n- An ordinary interface is used unless `expect/actual` provides a concrete static benefit.\n- Every abstraction has a real caller and removes a measurable dependency.\n- Native or IPC exports are smaller and more stable than the internal Kotlin API.\n\n## Module and source-set graph\n\n- The app host owns implementation aggregation, DI, navigation/window lifecycle and packaging.\n- Core does not import Feature; Feature does not import another Feature implementation.\n- Shared modules do not depend on platform implementations or renderer modules.\n- Gradle `api` exposure is intentional; otherwise use `implementation`.\n- Package colocation, Android module extraction, KMP conversion and renderer sharing remain separate\n  changes unless evidence requires combining them.\n- `commonMain` is compiled by at least one non-Android target before being called shared.\n- JVM+Android-only sharing has an honest owner and is not mislabeled as common.\n- Platform files live in the narrowest relevant source set.\n- Android services, Context/URI/resources and notifications remain Android-side.\n- Rhino/JS behavior remains behind a capability boundary until another target has a compatible,\n  tested implementation.\n\n## Renderer and host checks\n\n- Presentation contracts contain no Compose, Material/Miuix, Fluent, WinUI, SwiftUI or platform\n  resource types.\n- A renderer owns its resources, accessibility, layout and navigation presentation.\n- Shared Compose UI emits semantic callbacks/effects; the host owns platform launchers.\n- A CMP Screen has at least two intentional consumers or another documented product reason.\n- WinUI/iOS bridges define versioning, lifecycle, cancellation, error and memory ownership.\n- A sidecar protocol defines startup, readiness, shutdown, crash recovery, upgrade and local-access\n  behavior.\n\n## Gates\n\n- Affected Android unit/lint/architecture/package gates pass.\n- Common tests and actual metadata/target compile tasks pass.\n- Capability status distinguishes compile, contract-test, smoke, package and release-ready.\n- Adapter tests cover success, failure and cancellation where relevant.\n- Serialization/database changes include backward/forward or migration evidence.\n- Reader/rule/service changes include parity and, where relevant, real-device performance evidence.\n- Reduced historical violations lower their baseline in the same change.\n- `git diff --check` passes.\n\n## Scaffolding\n\n- At least two accepted manual examples prove a convention before it is generated.\n- Dry-run is available and default execution refuses overwrites.\n- Existing graph/DI files are amended safely rather than replaced wholesale.\n- Only necessary files are generated; no empty layers or speculative targets.\n- Generator behavior has fixture, snapshot or compilation evidence.\n\n## Review output\n\nFor each finding give a tight file/line reference, concrete impact and smallest credible fix:\n\n- P0/P1: behavior/data loss, incompatible rule/storage/ABI semantics, broken target, lifecycle,\n  thread, cancellation, native ownership or packaging defects.\n- P2: illegal dependency, renderer/platform leakage, false capability, state duplication, baseline\n  relaxation or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf no issue is found, state the remaining unverified host, device, package, interop and performance\nrisks.\n",".codex/skills/legado-compose-migration/SKILL.md":"---\nname: legado-compose-migration\ndescription: Guide Legado Android UI migration from XML/View/RecyclerView/DialogFragment screens to Jetpack Compose and Material 3, and guide new Compose-first screens using standard Android architecture. Use when Codex is asked to create, migrate, rewrite, review, or plan a Legado screen, Activity, Fragment, dialog, adapter, navigation destination, or settings page, especially when MainActivity navigation, MVI/UDF, StateFlow, Koin ViewModels, domain/usecase boundaries, or existing Compose component conventions matter.\n---\n\n# Legado Compose Migration\n\n## Overview\n\nMigrate one UI surface at a time, or create one new Compose destination at a time. Preserve behavior first for migrations; for newly created screens, prefer standard modern Android architecture over mixed legacy patterns. Use the project's existing `MainActivity` navigation, `BaseComposeActivity` compatibility hosts, `*Screen`, `*Contract`, `*ViewModel`, `StateFlow`, `SharedFlow`, Koin, theme, and widget patterns.\n\nBefore editing, inspect the target View implementation if one exists, `MainActivity` navigation when adding a destination, and at least two nearby migrated Compose screens. For concrete project patterns, including current Compose state/performance rules, read `references/project-patterns.md`.\n\n## Workflow\n\n1. State assumptions and success criteria.\n   - Name the exact screen or destination being created/migrated.\n   - For migrations, define behavior that must remain unchanged: inputs, result codes, navigation, menu actions, dialogs/sheets, list selection, refresh, persistence, and event bus behavior.\n   - For new screens, define the route owner, UI state, events/effects, domain/usecase boundary, and verification target.\n   - If the target mixes UI and business logic heavily, keep the migration surgical and defer deeper domain cleanup unless needed.\n\n2. Map the old surface.\n   - For migrations, read the Activity/Fragment, XML layouts, adapters, menu XML, dialogs, result launchers, and ViewModel.\n   - For new screens, read `MainActivity`, nearby route screens, the relevant ViewModel/usecase/repository patterns, and shared UI components.\n   - List UI state, user intents, one-shot effects, and external side effects.\n   - Identify reusable Compose components under `ui/widget/components` before creating new components.\n\n3. Choose the minimal migration shape.\n   - For new Compose-first screens, add a `MainActivity` navigation destination instead of creating a standalone Activity.\n   - Use `BaseComposeActivity` for full-screen Activity migrations only when a legacy Activity must remain as an entry point.\n   - Keep existing Activity Result APIs, `Intent` extras, permission flows, and Android framework calls in the host/compatibility Activity.\n   - If an unreworked View screen still starts the migrated screen with `Intent`, keep the old Activity only as a compatibility host that parses legacy extras and delegates to the Compose screen or `MainActivity` route boundary.\n   - Put renderable state in `UiState`, user actions in `Intent`, and one-shot navigation/framework work in `Effect`.\n   - For new screens, use standard Android/Compose architecture: UDF/MVI-style state hoisting, lifecycle-aware Flow collection, ViewModel-owned state, repository/usecase boundaries, and UI free of business logic.\n   - Use mixed legacy patterns only as an integration boundary for unreworked View screens or existing framework contracts.\n   - Use existing repositories/usecases when they already fit; introduce new domain/usecase classes when a new screen needs clean business boundaries or when a migration would otherwise duplicate or entangle business logic.\n\n4. Implement by layers.\n   - Create or update `FeatureContract.kt` first for `UiState`, `Intent`, `Effect`, dialog/sheet models, and menu action enums.\n   - Update `FeatureViewModel.kt` to expose `uiState: StateFlow<UiState>` and `effects: SharedFlow<Effect>`, with a single `onIntent(...)` entry point unless the existing feature has a simpler established pattern.\n   - Create `FeatureScreen.kt` as a stateless route-level composable: `state`, callbacks, and `onIntent`.\n   - Wire new Compose destinations through `MainActivity` route handling; update a retained Activity only when legacy `Intent` compatibility is required.\n   - Collect state with `collectAsStateWithLifecycle()` and collect effects in `LaunchedEffect(Unit)` from the route or compatibility host.\n   - Register new ViewModels in `di/appModule.kt` with `viewModelOf(::FeatureViewModel)` unless parameters require `viewModel { ... }`.\n\n5. Remove only obsolete migration artifacts.\n   - Delete XML layouts, adapters, menu resources, binding fields, and imports only when the migrated screen no longer references them.\n   - Do not refactor unrelated View screens or shared utilities.\n\n6. Verify.\n   - Prefer the smallest Gradle check that compiles the touched app code, usually `.\\gradlew.bat :app:compileAppDebugKotlin`.\n   - If resources, manifests, or XML deletion are involved, run `.\\gradlew.bat :app:assembleAppDebug` when feasible.\n   - For behavior-heavy changes, add or update focused tests only where the project already has a practical test seam.\n\n## Boundaries\n\n- Keep Compose functions side-effect-light. Use `LaunchedEffect` for collecting effects and use callbacks for user actions.\n- Do not pass `Activity`, `View`, binding objects, or mutable domain entities deep into composables unless an existing local pattern requires it.\n- Prefer project theme/components: `LegadoTheme`, `AppScaffold`, `ListScaffold`, `AppAlertDialog`, `AppModalBottomSheet`, `RoundDropdownMenu`, top bar helpers, setting items, cover components, and list utilities.\n- Prefer `StateFlow`/`SharedFlow` over `LiveData` for newly migrated Compose surfaces.\n- When using `FeatureIntent` for MVI user actions, distinguish it from Android `Intent` extras and launch APIs in names, comments, and explanations where both appear.\n- For new Compose-first screens, do not copy View-era shortcuts such as UI logic in Activity/Fragment, direct binding-like mutable UI state, adapter-owned state, or Activity-context business operations.\n- Keep one-off Android actions out of `UiState`: navigation, file opening, clipboard, dialogs implemented as Android DialogFragments, result launchers, permission requests, and callbacks that require host context should be `Effect`s handled by `MainActivity` route handling or a compatibility Activity.\n- Prefer `MainActivity` navigation for new Compose destinations. Treat standalone Activities for migrated screens as legacy entry points only when existing View code still depends on `Intent` navigation.\n- Keep existing Chinese string resources and localization behavior; add strings to resources when user-facing text is new.\n\n## Reference\n\nRead `references/project-patterns.md` when implementing or reviewing a migration. It contains project-specific examples, file placement rules, state/effect conventions, and verification commands.\n",".codex/skills/legado-compose-migration/agents/openai.yaml":"interface:\n  display_name: \"Legado Compose Migration\"\n  short_description: \"Guide Legado Compose screens and migration\"\n  default_prompt: \"Use $legado-compose-migration to create or migrate a Legado Android Compose screen with MainActivity navigation and standard architecture.\"\n",".codex/skills/legado-compose-migration/references/project-patterns.md":"# Legado Compose Migration Project Patterns\n\n## Existing Shape\n\n- App module: `app`.\n- Main source root: `app/src/main/java/io/legado/app`.\n- UI package: `ui/...`, usually grouped by feature.\n- Data package: `data/dao`, `data/entities`, `data/repository`.\n- Domain package: `domain/gateway`, `domain/model`, `domain/repository`, `domain/usecase`.\n- DI: `di/appModule.kt`, using Koin `singleOf`, `viewModelOf`, and parameterized `viewModel { ... }`.\n- Main navigation: `ui/main/MainActivity.kt`, using Navigation3 `NavKey`, `rememberNavBackStack`, `entryProvider`, and `NavDisplay`, plus legacy route extras such as `EXTRA_START_ROUTE`.\n- Base Compose host: `base/BaseComposeActivity.kt`, which wraps `Content()` in `AppTheme`, configures system bars, locale/font, background image, and LiveBus recreation events.\n- Compose dependencies are already enabled in `app/build.gradle.kts`; do not add new UI libraries unless the target screen truly requires one.\n- `kotlinx.collections.immutable` is available for Compose-facing screen state. Prefer `ImmutableList`, `ImmutableSet`, and `ImmutableMap` at `UiState` boundaries when collection state is passed to composables and changes often.\n\n## Files to Inspect Before Migrating\n\nInspect these as local examples, not as APIs to copy blindly:\n\n- `ui/book/info/BookInfoActivity.kt`\n- `ui/book/info/BookInfoContract.kt`\n- `ui/book/info/BookInfoViewModel.kt`\n- `ui/book/info/BookInfoScreen.kt`\n- `ui/book/search/SearchContract.kt`\n- `ui/book/search/SearchViewModel.kt`\n- `ui/book/search/SearchScreen.kt`\n- `ui/main/MainActivity.kt`\n- `ui/main/MainScreen.kt`\n- `ui/widget/components/AppScaffold.kt`\n- `ui/widget/components/list/ListScaffold.kt`\n- `ui/theme/LegadoTheme.kt`\n- `ui/theme/AppTheme.kt`\n\n## Recommended Feature Layout\n\nFor a migrated feature, prefer colocated files:\n\n```text\nui/<area>/<feature>/\n  FeatureActivity.kt       # only for legacy Intent compatibility or full-screen migration host\n  FeatureContract.kt\n  FeatureViewModel.kt\n  FeatureScreen.kt\n  FeatureSheets.kt        # only if sheets are substantial\n  FeatureDialogs.kt       # only if dialogs are substantial\n```\n\nUse the smallest set of files. Do not split files just to match this shape.\n\n## MVI and UDF Rules\n\nUse this shape for behavior-heavy screens:\n\n```kotlin\ndata class FeatureUiState(\n    val isLoading: Boolean = false,\n    val items: List<Item> = emptyList(),\n    val dialog: FeatureDialog? = null,\n    val sheet: FeatureSheet = FeatureSheet.None,\n)\n\nsealed interface FeatureIntent {\n    data object BackPressed : FeatureIntent\n    data object Refresh : FeatureIntent\n    data class ItemClick(val id: Long) : FeatureIntent\n}\n\nsealed interface FeatureEffect {\n    data object Finish : FeatureEffect\n    data class OpenDetail(val id: Long) : FeatureEffect\n}\n```\n\nViewModel rules:\n\n- Keep `_uiState = MutableStateFlow(FeatureUiState())` private and expose `uiState = _uiState.asStateFlow()`.\n- Keep `_effects = MutableSharedFlow<FeatureEffect>(extraBufferCapacity = 8)` private and expose `effects = _effects.asSharedFlow()`.\n- Prefer `fun onIntent(intent: FeatureIntent)` as the UI entry point.\n- Treat `FeatureIntent` as an MVI/user-action type. When Android `Intent` launch/extras are also involved, name variables and explanations clearly enough that the two concepts are not confused.\n- Use `_uiState.update { it.copy(...) }` for state changes.\n- Emit one-shot work through effects, not booleans in state.\n- Keep cached current entities private in the ViewModel when existing project behavior needs mutation or incremental sync, but publish immutable render state.\n- For collection-heavy `UiState`, convert DAO/repository `List`/`Set`/`Map` values to `kotlinx.collections.immutable` at the ViewModel/UI-state boundary with `toImmutableList()`, `toImmutableSet()`, or `toImmutableMap()`. Do not force repository, DAO, or domain APIs to use persistent collections unless the domain contract truly benefits.\n- Keep internal flow pipelines free to use normal Kotlin collections for sorting, grouping, and persistence work; the immutable collection rule is primarily for values exposed to Compose.\n- Use existing `BaseViewModel.execute { ... }.onSuccess { ... }.onError { ... }` when the surrounding ViewModel already uses that pattern.\n\nActivity rules:\n\n- Prefer `MainActivity` as the owner of Compose-first navigation. Add a Navigation3 route/key and `entryProvider` entry there for new destinations.\n- Extend `BaseComposeActivity` for migrated Activity screens only when a standalone Activity remains necessary.\n- Keep a legacy Activity for a migrated screen only when unreworked View code still calls it through `Intent`; in that case, make the Activity a thin compatibility host that translates extras/results and delegates to Compose state/effects.\n- Keep Activity Result launchers, file openers, clipboard, permission APIs, Android DialogFragments, and framework navigation in the Activity.\n- In `Content()`, collect state with `collectAsStateWithLifecycle().value`.\n- Collect effects in `LaunchedEffect(Unit) { viewModel.effects.collectLatest { ... } }`.\n- Pass only `state` and callbacks into the screen.\n\nNavigation rules:\n\n- For new Compose-first destinations, add the route to `MainActivity` instead of adding a new Activity.\n- Follow `MainActivity`'s current route style: serializable `NavKey` routes for in-process navigation and launcher `Intent` helper methods only when external or legacy callers need a stable entry point.\n- Keep legacy extra names stable when replacing an old Activity entry point.\n- When a compatibility Activity is retained, let it translate old `Intent` inputs/results; do not make it the source of truth for navigation or business behavior.\n\nScreen rules:\n\n- Make `FeatureScreen(state, onIntent, ...)` stateless for business state.\n- Use `remember` and `rememberSaveable` only for local UI affordances such as menu expansion, scroll state, transient animation state, and text field drafts when committing through an intent.\n- Use `rememberSaveable` for user-driven transient targets that should survive recreation, such as a delete-confirmation item ID or URL. Store only the stable ID in saved state and resolve the current entity from `UiState`.\n- Use `BackHandler` to route back actions through `FeatureIntent.BackPressed` when ViewModel decisions matter.\n- Keep expensive derived values in `remember(key)` or ViewModel state when they depend on repository data.\n- Prefer `LazyColumn`, `LazyRow`, stable keys, and existing fast-scroller/list components for adapter migrations. For heterogeneous lazy lists or grids, provide `contentType` in addition to `key`.\n- In long-lived `LaunchedEffect` collectors, wrap changing callbacks, `Context`-dependent operations, or lambdas from parents with `rememberUpdatedState` when the effect should not restart.\n- Hoist state only to the lowest common owner that reads and writes it. Move branch-only ViewModel lookup and Flow collection into the branch or a small child composable instead of collecting at a high-level route.\n- Avoid UI-state feedback loops such as `LaunchedEffect(uiState.items) { viewModel.pruneSelection(...) }`. Prefer deriving consistency inside the ViewModel with `combine(...)`, or reduce the state as part of the flow that produces the data.\n\n## Current Compose Performance Notes\n\n- Kotlin 2.x enables strong skipping by default, but ordinary Kotlin `List`, `Set`, and `Map` are still unstable to Compose. This means high-frequency screen state with normal collections can still widen recomposition work.\n- Use immutable collections for render state that crosses into Compose. Do not mechanically replace every temporary collection, Room query result, or internal mutable accumulator.\n- If data entities come from modules where the Compose compiler does not run, consider a stable UI model wrapper when the entity is passed deeply through composables and causes measurable recomposition cost.\n- Treat `SnapshotStateList` / `SnapshotStateMap` as UI-owned mutable state, not as a default ViewModel `UiState` transport type.\n\n## Clean Architecture Boundaries\n\nThis project is mid-migration, so use Clean Architecture pragmatically. For newly created screens, default to standard modern Android architecture and avoid mixed legacy patterns unless they are needed to interoperate with View screens that have not been rewritten.\n\n- For new screens, keep UI, presentation, domain, and data boundaries explicit: Compose renders state, ViewModel owns state and intent handling, usecases hold reusable business actions, repositories mediate data access.\n- For new screens, do not place business rules in Activity/Fragment or composables.\n- For new screens, do not read DAOs directly from UI/presentation unless the project already has no reasonable repository/usecase boundary and adding one would be disproportionate.\n- For migrations, use existing DAOs/repositories directly if the old ViewModel already does and the migration is UI-only.\n- Prefer existing `domain/usecase` classes for reusable business actions such as cache, delete, group update, startup maintenance, reading progress, and WebDAV flows.\n- Add a usecase when a new screen has meaningful business rules, when logic would otherwise be duplicated across features, or when a UI migration needs to extract meaningful business rules from an Activity/adapter.\n- Keep UI-only formatting and Compose layout decisions out of domain.\n- Keep entity-to-display mapping near the UI or ViewModel unless it is reused business language.\n\n## Compose Components and Theme\n\nPrefer project components before raw Material widgets:\n\n- Scaffold/top bars: `AppScaffold`, `GlassTopAppBar...`, `DynamicTopAppBar`, `TopBarActionButton`, `TopBarNavigationButton`.\n- Lists: `ListScaffold`, `TopFloatingStickyItem`, `VerticalFastScroller`, `SelectionBottomBar`.\n- Dialog/sheet: `AppAlertDialog`, `AppModalBottomSheet`, `OptionSheet`, text list input components.\n- Settings: `ClickableSettingItem`, `SwitchSettingItem`, `SliderSettingItem`, `ListSettingItem`, `SettingCard`.\n- Buttons/chips: `AppIconButton`, `SmallIconButton`, `SmallTextButton`, `ToggleChip`, `AlertButton`.\n- Covers/images: `CoilBookCover`, `BookshelfCover`, `buildCoverImageRequest`; use Coil for Compose image loading.\n- Theme: `LegadoTheme.colorScheme`, `LegadoTheme.typography`, `ThemeResolver`, `ProvideThemeOverride` where existing feature behavior needs theme override.\n\nAvoid introducing a second visual system. If a raw Material 3 component is used, style it from `LegadoTheme`.\n\n## Migration Checklist\n\nBefore editing:\n\n- Identify old XML layouts, ViewBinding fields, adapters, decorations, menu XML, dialogs, and result contracts.\n- Capture behavior: empty/loading/error states, refresh, search, sort, selection, long click, swipe, menu actions, back behavior, result codes, events, and persisted preferences.\n- Decide whether the task is UI-only or needs domain extraction.\n- For new screens, explicitly decide the presentation/domain/data boundary before coding; do not default to legacy mixed architecture.\n\nDuring editing:\n\n- Create/update `Contract`, then `ViewModel`, then `Screen`, then `MainActivity` route or compatibility host.\n- For new Compose destinations, wire navigation through `MainActivity` unless a legacy `Intent` caller must remain supported.\n- When retaining an Activity for compatibility, keep it small: parse legacy extras, collect effects, bridge result codes, and avoid placing feature business logic there.\n- Reuse existing string resources and add new resources for new user-facing text.\n- Register the ViewModel in `di/appModule.kt`.\n- Remove only resources made obsolete by this migration.\n- Keep existing public intent extra names and result codes unless explicitly changing an API.\n\nVerification:\n\n- Run `.\\gradlew.bat :app:compileAppDebugKotlin` for Kotlin-only changes.\n- Run `.\\gradlew.bat :app:assembleAppDebug` when resources, manifests, XML deletion, or generated bindings are affected.\n- If a screen uses Room queries or migration-sensitive repositories, run the relevant existing Android/unit tests if practical.\n- For manual verification, open the migrated screen and compare core flows against the old behavior list.\n\n## Common Pitfalls\n\n- Do not put navigation or Activity launchers inside composables.\n- Do not store one-time navigation as nullable fields in `UiState` unless the existing local pattern already does; prefer `SharedFlow` effects.\n- Do not keep adapter selection state split between old adapter and new Compose state.\n- Do not delete XML/menu resources until all references and generated binding imports are gone.\n- Do not add speculative domain layers for a single migrated screen.\n- Do not use mixed architecture for new screens unless compatibility with unreworked View UI requires it.\n- Do not make a migrated screen's retained Activity the primary navigation model when `MainActivity` can own the Compose destination.\n- Do not bypass `BaseComposeActivity` unless the screen must remain inside a Fragment or legacy host.\n- Do not replace existing event bus or service interactions as part of a UI migration unless required for correctness.\n",".codex/skills/legado-compose-review/SKILL.md":"---\nname: legado-compose-review\ndescription: Review existing Legado Jetpack Compose code for architecture, behavior, maintainability, and project convention issues. Use when Codex is asked to audit, review, inspect, evaluate, or find problems in Legado Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, or early Compose implementations, especially for MVI/UDF, StateFlow/SharedFlow, Clean Architecture, MainActivity navigation, legacy Activity compatibility, and View-era mixed-pattern drift.\n---\n\n# Legado Compose Review\n\n## Overview\n\nReview existing Compose code before rewriting it. Focus on concrete defects, architectural drift, behavior risks, and missing verification, especially in early Compose screens that may predate the current MVI/UDF, Clean Architecture, and `MainActivity` navigation expectations.\n\nRead `references/review-checklist.md` for the project-specific checklist, current Compose state/performance checks, and severity guidance.\n\n## Workflow\n\n1. Define the review scope.\n   - Identify the exact screen, route, ViewModel, contract, or package under review.\n   - State whether the user wants review only or review plus fixes. If unclear, review first and do not edit code.\n   - Treat unrelated legacy View code as context, not as part of the review, unless it affects the Compose surface.\n\n2. Build context from code.\n   - Read the `*Screen`, `*ViewModel`, `*Contract`, host Activity/route, DI registration, and any repositories/usecases used by the feature.\n   - Read the old View implementation only if it is still a compatibility caller or behavior reference.\n   - Compare against nearby current examples such as `BookInfo*`, `Search*`, `MainActivity`, and shared components under `ui/widget/components`.\n\n3. Review by risk, not style preference.\n   - Prioritize behavior regressions, state duplication, lifecycle bugs, navigation bugs, business logic in UI, direct data access from UI/presentation, recomposition hazards, and missing compatibility handling.\n   - Flag style-only issues only when they conflict with established project conventions or make future migration harder.\n   - Do not require broad refactors for a small screen unless the current code creates real behavior or maintenance risk.\n\n4. Output findings first.\n   - Use code-review style: list findings before summaries.\n   - Include file and tight line references.\n   - Explain the concrete impact and the smallest credible fix.\n   - If using Codex review directives, emit one `::code-comment{...}` per finding.\n   - If no issues are found, say that clearly and mention remaining test/manual verification gaps.\n\n5. Suggest fixes only after findings.\n   - Group fixes into small, reviewable steps.\n   - For architecture drift, separate compatibility-preserving fixes from larger cleanup.\n   - Recommend `legado-compose-migration` only when the finding implies a migration or rewrite workflow.\n\n## Review Priorities\n\n- **P0/P1**: behavior breakage, lost navigation/result behavior, unsafe lifecycle collection, stale state, data corruption, crashes, or compatibility entry points that no longer work.\n- **P2**: architecture drift that will cause duplicated logic, hard-to-test behavior, recomposition bugs, or incorrect ownership of state/effects.\n- **P3**: convention drift, maintainability issues, weak naming, missing previews/tests, or small cleanup that should not block behavior.\n\n## Boundaries\n\n- Do not rewrite code during a review unless the user asks for fixes.\n- Do not demand pure architecture where a compatibility boundary is necessary for unreworked View screens.\n- For new Compose-first code, expect standard Android architecture: `MainActivity` route ownership, ViewModel-owned state, UDF/MVI user actions, `StateFlow`/`SharedFlow`, repositories/usecases, and UI without business logic.\n- For migrated code, allow a thin retained Activity only for Android `Intent` compatibility with unreworked View callers.\n- Distinguish MVI `FeatureIntent` user actions from Android `Intent` launch/extras when both appear.\n\n## Reference\n\nRead `references/review-checklist.md` for detailed checks. When a review turns into implementation, also read `../legado-compose-migration/references/project-patterns.md` if available.\n",".codex/skills/legado-compose-review/agents/openai.yaml":"interface:\n  display_name: \"Legado Compose Review\"\n  short_description: \"Review Legado Compose architecture quality\"\n  default_prompt: \"Use $legado-compose-review to review a Legado Compose screen for MVI, UDF, Clean Architecture, navigation, and project convention issues.\"\n",".codex/skills/legado-compose-review/references/review-checklist.md":"# Legado Compose Review Checklist\n\n## Files to Read\n\nFor a feature review, inspect the smallest complete slice:\n\n- `FeatureScreen.kt` and any `FeatureSheets.kt` / `FeatureDialogs.kt`.\n- `FeatureViewModel.kt`.\n- `FeatureContract.kt` if present.\n- Host route in `MainActivity.kt` or retained `FeatureActivity.kt`.\n- Koin registration in `di/appModule.kt`.\n- Repositories/usecases used by the feature.\n- Old View caller or XML/adapters only when still used for compatibility or behavior comparison.\n\nUse current examples as references:\n\n- `ui/main/MainActivity.kt` for Navigation3 route ownership.\n- `ui/book/info/BookInfoContract.kt`, `BookInfoViewModel.kt`, `BookInfoScreen.kt` for behavior-heavy MVI/UDF.\n- `ui/book/search/SearchContract.kt`, `SearchViewModel.kt`, `SearchScreen.kt` for route-level search state.\n- `ui/widget/components/...` and `ui/theme/...` for shared UI conventions.\n\n## Architecture Checks\n\nFlag issues when:\n\n- A new Compose-first screen is implemented as a standalone Activity instead of a `MainActivity` destination without a compatibility reason.\n- A retained Activity contains feature business logic instead of acting as a thin Android `Intent` compatibility host.\n- Business rules, repository calls, DAO calls, persistence writes, or service orchestration live in composables.\n- ViewModel exposes mutable state directly, exposes `MutableStateFlow`, or lets UI mutate domain objects.\n- UI state is split across Activity fields, composable `remember` state, adapter state, and ViewModel state in a way that can diverge.\n- One-shot events such as navigation, result codes, file opening, permission requests, or clipboard writes are represented as persistent `UiState` fields that can replay incorrectly.\n- MVI `FeatureIntent` user actions are confused with Android `Intent` launch/extras.\n- Domain/usecase boundaries are bypassed in new screens when a meaningful business action exists.\n- A migration introduces new domain abstractions for a single trivial UI action without reducing real complexity.\n\n## UDF and State Checks\n\nFlag issues when:\n\n- `Screen` functions own business state instead of receiving `state` and callbacks.\n- `remember` / `rememberSaveable` stores source-of-truth data that should survive process or route recreation through ViewModel state.\n- User-driven transient state that should survive recreation, such as a pending delete-confirmation target, is held with plain `remember` instead of `rememberSaveable` or ViewModel state.\n- `LaunchedEffect` keys are unstable or cause repeated data loading, duplicate navigation, duplicate toasts, or repeated service calls.\n- A long-lived `LaunchedEffect` collector captures parent callbacks or context-dependent values that can change without using `rememberUpdatedState`, unless the effect is intentionally keyed to restart.\n- Flows are collected without lifecycle awareness in UI routes where `collectAsStateWithLifecycle()` should be used.\n- List items lack stable keys where mutation, selection, or animation can make state attach to the wrong row.\n- Heterogeneous lazy lists or grids provide `key` but omit `contentType`, reducing composition reuse quality when headers, rows, ads, loading items, or expanded content mix in the same lazy layout.\n- Selection, search query, sorting, filtering, loading, and error states are not represented in a single coherent `UiState`.\n- Derived values are recomputed expensively on every recomposition instead of living in ViewModel state or `remember(key)`.\n- UI effects write list-derived consistency back into the ViewModel, such as pruning selection from `LaunchedEffect(uiState.items)`. Prefer deriving this in the ViewModel with `combine(...)` or reducing it when the data flow updates.\n- State is hoisted higher than the lowest common owner, such as a top-level route collecting a child ViewModel flow only needed inside a conditional branch or one popup menu.\n\n## Compose Stability and Collection Checks\n\nFlag issues when:\n\n- Collection-heavy `UiState` exposed to Compose uses ordinary Kotlin `List`, `Set`, or `Map` in hot paths. Kotlin 2.x strong skipping is helpful but does not make standard Kotlin collections stable to Compose.\n- DAO or repository lists are passed directly through `UiState` to deep composables without conversion to `ImmutableList` / `ImmutableSet` / `ImmutableMap` or a stable UI model wrapper.\n- Review recommendations imply replacing every internal collection. Keep the finding scoped to Compose-facing render state; temporary accumulators, sorting inputs, Room DAO signatures, and domain APIs can remain normal collections unless they are the actual recomposition boundary.\n- Mutable collections such as `ArrayList`, `MutableList`, or mutable maps are stored in Compose state or ViewModel `UiState`.\n- Entity classes from non-Compose modules are passed deeply through composables and cause visible recomposition churn; prefer stable UI render models if measurement or code shape shows this matters.\n\n## Navigation and Compatibility Checks\n\nFlag issues when:\n\n- New Compose destinations skip `MainActivity` Navigation3 route registration.\n- Legacy Android `Intent` extras or result codes change without an explicit compatibility plan.\n- A migrated screen is reachable both through `MainActivity` and a retained Activity with inconsistent state initialization.\n- Navigation is performed directly inside nested composables instead of through callbacks/effects.\n- Activity Result launchers, file pickers, permission requests, or Android DialogFragments are hidden inside reusable UI composables.\n- Back behavior bypasses ViewModel decisions when unsaved changes, selection mode, add-to-shelf prompts, or confirmation dialogs exist.\n\n## UI and Project Convention Checks\n\nFlag issues when:\n\n- Raw Material components ignore `LegadoTheme` or existing shared components where the project already has a wrapper.\n- A screen recreates shared UI already present under `ui/widget/components`.\n- User-facing text is hardcoded instead of using string resources, except for temporary debug-only text.\n- Compose UI keeps ViewBinding, adapter, or XML assumptions after migration.\n- Dialogs and bottom sheets use inconsistent project components when `AppAlertDialog`, `AppModalBottomSheet`, or existing option sheets fit.\n- Image loading bypasses existing Coil cover/image helpers where cover behavior, cache, SVG, or GIF handling matters.\n\n## Clean Architecture Checks\n\nFor new screens, expect:\n\n- Compose renders state and emits user actions.\n- ViewModel owns state, intent handling, and effect emission.\n- Usecases own reusable business actions.\n- Repositories mediate data access.\n- DAOs remain behind repositories unless adding a boundary would be disproportionate and local project patterns already allow direct use.\n\nFor migrated screens, allow pragmatic intermediate code only when:\n\n- It preserves behavior.\n- It is isolated behind a clear compatibility boundary.\n- It does not spread View-era assumptions into new Compose-first code.\n\n## Output Template\n\nUse this shape unless the user asks for another format:\n\n```text\nFindings\n- [P1] Title\n  file:line\n  Impact: ...\n  Fix: ...\n\nOpen Questions\n- ...\n\nNotes\n- No issues found in ... / Tests not run ...\n```\n\nWhen using Codex inline review comments, emit one directive per finding with tight line ranges:\n\n```text\n::code-comment{title=\"[P2] Keep route state in ViewModel\" body=\"...\" file=\"/absolute/path/FeatureScreen.kt\" start=42 end=45 priority=2 confidence=0.8}\n```\n\n## Verification Suggestions\n\nRecommend verification based on the reviewed change:\n\n- Kotlin-only review/fix: `.\\gradlew.bat :app:compileAppDebugKotlin`.\n- Resource/XML/manifest impact: `.\\gradlew.bat :app:assembleAppDebug`.\n- Navigation or compatibility changes: manually open both `MainActivity` route and any retained legacy Android `Intent` entry point.\n- Behavior-heavy ViewModel changes: add or run focused tests only where the project has a practical test seam.\n",".github/copilot-instructions.md":"# Copilot repository instructions\n\nRead and follow `/AGENTS.md` before changing this repository. It is the source of truth for\nproject-wide constraints, source priority, validation, current Android boundaries, and the KMP/CMP\ndirection.\n\nFor specialized work, also read the matching repository skill:\n\n- `/.agents/skills/legado-compose-migration/SKILL.md` for View/XML to Compose or new Android Compose\n  screens.\n- `/.agents/skills/legado-compose-review/SKILL.md` for Compose architecture and behavior review.\n- `/.agents/skills/legado-kmp-migration/SKILL.md` for modularization, `commonMain`, CMP, platform\n  adapters, migration gates, and scaffolding.\n\nDo not infer current package names, Gradle tasks, dependencies, navigation APIs, or platform support\nfrom examples. Confirm them in source, tests, `settings.gradle`, Gradle build files, and\n`gradle/libs.versions.toml`. The KMP target structure in `docs/dev/kmp-cmp-modernization.md` is a\nstaged proposal, not a description of modules that already exist.\n\nKeep changes surgical, preserve existing workspace edits, run risk-matched verification, and report\nexact commands plus unverified behavior.\n","AGENTS.md":"# AGENTS.md\n\n本文件定义仓库级不变量。专项流程放在 `.agents/skills/`，长期方案放在 `docs/dev/`\n；不要把临时任务、讨论过程或一次性结论继续堆进本文件。\n\n## 资料优先级\n\n执行任务前按以下顺序建立上下文：\n\n1. 用户本次目标、范围与验收条件。\n2. 本文件的仓库级约束。\n3. 与任务匹配的 `.agents/skills/*/SKILL.md` 及其明确要求读取的 reference。\n4. `docs/dev/` 中的专项设计或迁移计划。\n5. 当前源码、测试、Gradle 配置和相邻实现；它们用于确认当前版本的真实 API 与行为。\n6. 外部资料只作为设计依据；涉及会变化的 Android、Kotlin、Compose 或库 API 时，以官方当前文档和本仓库版本为准。\n\n文档与源码不一致时，先指出差异。不要为了让实现符合过期文档而静默改代码。\n\n## Skill 路由\n\n- XML/View/Activity/Fragment/RecyclerView 到 Compose 的页面迁移、新 Compose 页面：使用\n  `legado-compose-migration`。\n- Compose Screen、Contract、ViewModel、导航、状态与兼容边界审查：使用 `legado-compose-review`。\n- Gradle 模块拆分、`commonMain` 抽取、KMP/CMP、`expect/actual`、平台能力适配、迁移门禁或相关脚手架：使用\n  `legado-kmp-migration`。\n- 单个任务可组合 skill，但只读取与当前工作切片有关的 reference。\n\n## 工作方式\n\n- 开始前说明假设、成功条件和不在范围内的内容。存在多种会显著改变结果的解释时，不静默选择。\n- 优先做最小、可回滚、可独立验证的垂直切片；不得借任务重构无关代码。\n- 修 bug 时先建立可复现测试或其他可观察证据；重构前后保持同一行为验证通过。\n- 保留工作区中用户已有修改。只移除本次改动产生的无用代码和资源。\n- 不为了“架构完整”创建空模块、单方法包装、无调用方抽象或通用 `Utils/Common` 容器。\n\n## 当前工程事实与目标方向\n\n当前工程是 Android-first 的渐进迁移仓库，不是已经完成的 KMP 工程：\n\n- Gradle 模块：`:app`、`:modules:book`、`:modules:rhino`、`:baselineprofile`；`modules/web` 是独立 Vue 3\n  工程。\n- `:app` 同时包含 Android UI、数据、领域、服务和大量遗留 View 代码；包目录只表达逻辑边界，尚无 Gradle\n  编译隔离。\n- 新 UI 以 Jetpack Compose、Material 3、Navigation 3、Koin、StateFlow/SharedFlow 为默认；阅读器渲染核心等成熟\n  View 代码可作为 Android 专业渲染岛保留。\n- `domain/` 是迁移目标边界，但当前仍存在 Android/JVM 类型渗透；不要把“目标纯净度”描述成已完成事实。\n- 长期目标是渐进演进为 KMP/CMP。详细目标结构、阶段和门禁见 `docs/dev/kmp-cmp-modernization.md`。\n\n### Feature-first 过渡结构\n\n- 新 Compose-first Feature 和完成整页迁移的代码使用 `io.legado.app.feature.<name>`；目录、Gradle\n  path、Kotlin package 分别采用 `feature/book-info`、`:feature:book-info`、`feature.bookinfo`。\n- 在 `:app` 内先形成稳定 Feature 包，再提升为真实 `:feature:<name>` 模块；只有确有多宿主/多模块消费者时才拆\n  `api/impl`，只有确有跨平台价值时才改为 KMP/CMP source sets。\n- Feature 内共置 Contract、ViewModel、Route、Screen 和私有 components/dialog/sheet/model；文件的详细归属与迁移步骤见\n  `docs/dev/feature-first-structure.md`，现有业务 owner 见 `docs/dev/feature-catalog.md`。\n- App host 聚合导航和 DI；Feature 不直接依赖其他 Feature 实现。领域、数据、平台能力不能为了目录整齐塞入\n  Feature UI 包。\n- 旧 `ui/...` 是渐进迁移区，不做全量机械搬家；一个 Feature 的新 owner 建立后，禁止继续向旧包新增同职责文件。\n\n### 目标依赖方向\n\n```text\napp host / composition root\n        ↓\nfeature implementation → feature API\n        ↓\ndomain / model / platform contracts\n        ↓\ndata abstractions\n        ↓\nplatform implementations (Android/JVM/iOS/...)\n```\n\n- 应用宿主负责组装 Feature 实现、DI、平台生命周期和导航运行时。\n- Feature 之间通过稳定契约或导航 API 协作，不直接依赖其他 Feature 的实现。\n- `commonMain` 只容纳经过依赖审计、能被至少一个非 Android 目标编译验证的代码；移动目录不等于完成跨平台迁移。\n- Android `Context`、Room 实体/DAO、Service、Broadcast、Activity Result、Cronet、文件\n  URI、通知、媒体会话、成熟阅读器渲染和现有 Rhino 集成默认留在平台侧，通过窄接口进入共享层。\n- 平台能力不可用时必须显式建模为 capability/unsupported，不得用静默空实现伪造跨平台支持。\n\n## Compose 屏幕约束\n\n- 新屏幕使用 Compose；不要新增 XML/View Activity/Fragment。\n- Android Compose 新屏幕采用 UDF/MVI：`@Stable XxxUiState`、`XxxIntent`、`XxxEffect`；ViewModel 直接继承\n  `ViewModel`，私有 `MutableStateFlow` / `MutableSharedFlow(extraBufferCapacity = 16)`，对外只暴露只读\n  Flow，并由单一 `onIntent` 分发用户动作。共享 Feature 的状态宿主按 KMP skill 单独决策。\n- Screen/Content 保持无业务逻辑、无 DAO/网络/存储直连；ViewModel 通过 Gateway/Repository/UseCase\n  访问业务能力。\n- 所有 UiState 与 UI item 数据类标注 `@Stable`。Compose 渲染边界中的集合使用\n  `kotlinx.collections.immutable`；内部计算和数据层不机械替换集合类型。\n- 导航、权限、文件选择和其他宿主动作通过回调或 Effect 处理。新目的地优先由 `MainActivity` 的 Navigation\n  3 图组装。\n- 使用项目的 `AppScaffold`、主题、top bar、dialog/sheet、列表和设置组件。正确处理 edge-to-edge 与\n  predictive back。\n- Activity 仅在遗留 `Intent`/result 兼容确有需要时作为薄宿主保留。\n\n具体文件形态和审查清单由 Compose migration/review skill 维护，本文件不复制完整模板。\n\n## 数据与设置边界\n\n- UI（包括 ViewModel）不得新增 DAO、`appDb.*Dao`、网络客户端或旧偏好 API 直连；历史债务由\n  `verifyConfigArchitecture` 的基线棘轮冻结并逐步下调。\n- 普通设置 Gateway 通过 `update { current -> current.copy(...) }` 修改状态；关联字段在一次\n  `copy(...)` 中原子提交。\n- 不引入 `*SettingsUpdate` 分发类型或 `updateAll`。\n- `ReadStyleMutation`、`ThemePackageSettingsGateway.applyAndAwait`、`ThemeStateTransaction`、\n  `AppUiConfigurationGateway` 等专用 API 保持现有形态。\n- Koin 中 Gateway 接口到 Repository 实现保持显式绑定；不要用构造函数绑定掩盖接口归属。普通\n  Repository/UseCase/ViewModel 可继续使用项目既有 `singleOf` / `viewModelOf` 约定。\n- 新的共享领域契约不得暴露 `Context`、`File`、`Uri`、Room 类型、Android resource id 或 JVM-only\n  stream；用领域值、`ByteArray`/抽象 source-sink、序列化模型或平台接口表达。\n\n## KMP/CMP 迁移纪律\n\n- 先写行为/契约测试和依赖清单，再移动代码；一个 PR 只完成一个可说明的边界变化。\n- 优先抽取稳定模型、纯函数、规则解析契约和 UseCase；网络、数据库、JS 引擎、服务、阅读器渲染最后通过平台适配器处理。\n- `expect/actual` 只用于真正的平台原语。可用普通接口 + DI 表达的能力，不使用 `expect/actual`。\n- 平台实现由 Koin 和应用 composition root 组装。\n- 不因 KMP 目标同时替换数据库、网络、DI、导航和 UI 框架。每次只改变一个主要风险维度。\n- Feature `api/impl` 只有形成真实 Gradle 边界并存在跨 Feature/宿主调用时才创建，不能只建同名文件夹。\n- 首个非 Android 目标是架构证明，不承诺功能齐平；目标顺序由 capability matrix 和产品需求决定。\n\n## 代码生成\n\n- 生成器用于编码已稳定的约定，不用于发明架构。至少有两个手工迁移且通过审查的同类样本后，才能固化模板。\n- 脚手架必须可预览或 dry-run、拒绝覆盖已有文件、输出最小文件集，并在已有 Feature 上采用追加/AST\n  感知修改，禁止覆盖 route graph 或 DI 文件。\n- 生成的可提交源码仍接受普通代码审查；真正的编译期生成物放入 `build/generated`，不得手工编辑或提交。\n- 模板和生成器变更必须有 fixture/snapshot 或编译验证，并与 convention plugin 的模块类型保持一致。\n\n## 重构审查门禁\n\n每个迁移 PR 至少回答：\n\n- 行为基线是什么，哪些测试或手工路径证明没有回退？\n- 依赖边界净变化是什么，是否减少了 Android/JVM 泄漏或历史基线？\n- 新模块/接口是否有真实调用方，能否用更小改动完成？\n- Android 实现与共享契约的错误、线程、取消、序列化语义是否一致？\n- 做了哪些验证，哪些真机、性能、外部服务或其他平台行为尚未验证？\n- CI 是否执行了受影响的共享测试、目标编译或 adapter contract test？\n\n减少历史违规时必须同步下调对应 baseline；门禁不接受“先放宽基线再迁移”。详细分级见 KMP/CMP 现代化计划。\n\n## 构建与验证\n\n开发与 CI 使用 JDK 21。按风险选择最小充分验证：\n\n```powershell\n# Kotlin/Compose 快速编译\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# 当前主验证集\n.\\gradlew.bat testAppDebugUnitTest lintAppDebug verifyConfigArchitecture assembleAppDebug --continue --no-configuration-cache\n\n# 资源、Manifest、生成绑定或打包变化\n.\\gradlew.bat :app:assembleAppDebug\n\n# Release/R8 相关\n.\\gradlew.bat assembleAppRelease\n.\\gradlew.bat assembleAppNoR8\n\n# Web 前端\npnpm --dir modules/web build\n```\n\nKMP 任务名在模块实际创建后才存在；不要假装运行尚未定义的 `commonTest`、目标编译或 API\n检查任务。新增模块时把其真实任务加入 CI，并在交付中列出准确命令。\n\n所有文本改动至少运行 `git diff --check`。构建通过不替代架构边界、行为和真机性能复核。\n\n## 重要项目约束\n\n- 不将 jsoup 升级到 1.16.2 以上；新版行为会影响 `AnalyzeByJSoup.kt` 与 JsoupXpath。\n- 应用代码仅在两处依赖 Hutool：①作为书源 JS 的运行时加密库保留在 classpath（书源直接调用\n  `Packages.cn.hutool.*`）；②`help/crypto/CryptoUtils.kt` 的 base64 **解码**统一走\n  `cn.hutool.core.codec.Base64.decode`，以兼容缺 `=` 填充等宽松输入（Kotlin\n  `kotlin.io.encoding.Base64` 严格填充会导致部分书源取章名/正文回归）。版本固定 5.8.22 勿升级。\n  应用内部加密主体仍用现有 JCA（`javax.crypto`/`java.security`）与 `help/crypto` 路径，KMP 抽取时\n  再通过能力契约替换 JVM API。\n- 代码 namespace 为 `io.legado.app`，Android `applicationId` 为 `io.legato.kazusa`，不要混用。\n- 当前 minSdk 26、target/compile SDK 37；Release 启用 R8 与资源压缩，`noR8` 变体用于排障。\n- Rhino 书源/RSS/TTS 规则、Android 服务、Web 服务和阅读器渲染属于高行为风险平台能力，迁移前必须建立兼容测试或\n  capability 边界。\n- `modules/web` 必须连接应用内 Ktor WebService；开发环境通过 `VITE_API` 指向设备服务地址。\n\n## 交付说明\n\n最终说明应包含：修改的职责范围、关键设计取舍、实际运行的验证与结果、未验证风险。不要把“已生成代码”或“已移动文件”当作完成标准。\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis is a Material Design 3 fork of [Legado](https://github.com/gedoor/legado). `app/src/main/java/io/legado/app/` uses **Clean Architecture** with three layers:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `:app`, `:modules:book` (epub/TXT parsing, namespace `me.ag2s`), `:modules:rhino` (Rhino JS wrapper, namespace `com.script`). There is also a Vue 3 web frontend in `modules/web/` (pnpm, separate from the Android build).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`MainActivity` holds a single `NavDisplay` with `entryProvider { ... }` defining all composable entries. `Launcher0` through `LauncherW` extend `MainActivity` to provide multiple launcher icon alias entries. Separate activities handle the reader (`ReadBookActivity` — still View-based), book info, source management, replace rules, file manager, QR scanner, etc.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 theme modes (`AppThemeMode` enum) — Dynamic (Monet), 12 named presets, Custom (MaterialKolor seed-color generation), Transparent. `CustomColorScheme` wraps `com.materialkolor` with configurable `PaletteStyle` (TonalSpot, Neutral, Vibrant, Expressive, Rainbow, etc.) and `ColorSpec` (2021 vs 2025).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.claude/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook sources, RSS sources, and HTTP TTS use JavaScript rules. `initRhino()` in `App.kt` registers `NativeBaseSource` wrappers for `BookSource`, `RssSource`, `HttpTTS` (writable JS objects) and `ReadOnlyJavaObject` wrappers for rule entities. Rule parsing logic lives in `help/source/` and `model/analyzeRule/`.\n\n## Important Constraints\n\n- **Do not update jsoup** beyond 1.16.2 — a breaking change in newer versions (see [jsoup#2017](https://github.com/jhy/jsoup/pull/2017)) affects `AnalyzeByJSoup.kt` and the JsoupXpath library\n- Hutool is back on the classpath at 5.8.22 (do not upgrade). Book-source JS calls it via\n  `Packages.cn.hutool.*`; app-internal **base64 decoding** in `help/crypto/CryptoUtils.kt` also\n  routes through `cn.hutool.core.codec.Base64.decode` for lenient input compatibility (Kotlin\n  `kotlin.io.encoding.Base64` is strict about `=` padding). App crypto otherwise uses JCA (\n  `javax.crypto`/`java.security`); new internal tools live in `help/crypto/CryptoUtils.kt`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated in `modules/web/` — a Vue 3 + TypeScript + Vite project for remote bookshelf and source editing. Must connect to the app's built-in HTTP server (started via `WebService` in the main activity settings). Commands:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\n本文件定义仓库级不变量。专项流程放在 `.agents/skills/`，长期方案放在 `docs/dev/`\n；不要把临时任务、讨论过程或一次性结论继续堆进本文件。\n\n## 资料优先级\n\n执行任务前按以下顺序建立上下文：\n\n1. 用户本次目标、范围与验收条件。\n2. 本文件的仓库级约束。\n3. 与任务匹配的 `.agents/skills/*/SKILL.md` 及其明确要求读取的 reference。\n4. `docs/dev/` 中的专项设计或迁移计划。\n5. 当前源码、测试、Gradle 配置和相邻实现；它们用于确认当前版本的真实 API 与行为。\n6. 外部资料只作为设计依据；涉及会变化的 Android、Kotlin、Compose 或库 API 时，以官方当前文档和本仓库版本为准。\n\n文档与源码不一致时，先指出差异。不要为了让实现符合过期文档而静默改代码。\n\n## Skill 路由\n\n- XML/View/Activity/Fragment/RecyclerView 到 Compose 的页面迁移、新 Compose 页面：使用\n  `legado-compose-migration`。\n- Compose Screen、Contract、ViewModel、导航、状态与兼容边界审查：使用 `legado-compose-review`。\n- Gradle 模块拆分、`commonMain` 抽取、KMP/CMP、`expect/actual`、平台能力适配、迁移门禁或相关脚手架：使用\n  `legado-kmp-migration`。\n- 单个任务可组合 skill，但只读取与当前工作切片有关的 reference。\n\n## 工作方式\n\n- 开始前说明假设、成功条件和不在范围内的内容。存在多种会显著改变结果的解释时，不静默选择。\n- 优先做最小、可回滚、可独立验证的垂直切片；不得借任务重构无关代码。\n- 修 bug 时先建立可复现测试或其他可观察证据；重构前后保持同一行为验证通过。\n- 保留工作区中用户已有修改。只移除本次改动产生的无用代码和资源。\n- 不为了“架构完整”创建空模块、单方法包装、无调用方抽象或通用 `Utils/Common` 容器。\n\n## 当前工程事实与目标方向\n\n当前工程是 Android-first 的渐进迁移仓库，不是已经完成的 KMP 工程：\n\n- Gradle 模块：`:app`、`:modules:book`、`:modules:rhino`、`:baselineprofile`；`modules/web` 是独立 Vue 3\n  工程。\n- `:app` 同时包含 Android UI、数据、领域、服务和大量遗留 View 代码；包目录只表达逻辑边界，尚无 Gradle\n  编译隔离。\n- 新 UI 以 Jetpack Compose、Material 3、Navigation 3、Koin、StateFlow/SharedFlow 为默认；阅读器渲染核心等成熟\n  View 代码可作为 Android 专业渲染岛保留。\n- `domain/` 是迁移目标边界，但当前仍存在 Android/JVM 类型渗透；不要把“目标纯净度”描述成已完成事实。\n- 长期目标是渐进演进为 KMP/CMP。详细目标结构、阶段和门禁见 `docs/dev/kmp-cmp-modernization.md`。\n\n### Feature-first 过渡结构\n\n- 新 Compose-first Feature 和完成整页迁移的代码使用 `io.legado.app.feature.<name>`；目录、Gradle\n  path、Kotlin package 分别采用 `feature/book-info`、`:feature:book-info`、`feature.bookinfo`。\n- 在 `:app` 内先形成稳定 Feature 包，再提升为真实 `:feature:<name>` 模块；只有确有多宿主/多模块消费者时才拆\n  `api/impl`，只有确有跨平台价值时才改为 KMP/CMP source sets。\n- Feature 内共置 Contract、ViewModel、Route、Screen 和私有 components/dialog/sheet/model；文件的详细归属与迁移步骤见\n  `docs/dev/feature-first-structure.md`，现有业务 owner 见 `docs/dev/feature-catalog.md`。\n- App host 聚合导航和 DI；Feature 不直接依赖其他 Feature 实现。领域、数据、平台能力不能为了目录整齐塞入\n  Feature UI 包。\n- 旧 `ui/...` 是渐进迁移区，不做全量机械搬家；一个 Feature 的新 owner 建立后，禁止继续向旧包新增同职责文件。\n\n### 目标依赖方向\n\n```text\napp host / composition root\n        ↓\nfeature implementation → feature API\n        ↓\ndomain / model / platform contracts\n        ↓\ndata abstractions\n        ↓\nplatform implementations (Android/JVM/iOS/...)\n```\n\n- 应用宿主负责组装 Feature 实现、DI、平台生命周期和导航运行时。\n- Feature 之间通过稳定契约或导航 API 协作，不直接依赖其他 Feature 的实现。\n- `commonMain` 只容纳经过依赖审计、能被至少一个非 Android 目标编译验证的代码；移动目录不等于完成跨平台迁移。\n- Android `Context`、Room 实体/DAO、Service、Broadcast、Activity Result、Cronet、文件\n  URI、通知、媒体会话、成熟阅读器渲染和现有 Rhino 集成默认留在平台侧，通过窄接口进入共享层。\n- 平台能力不可用时必须显式建模为 capability/unsupported，不得用静默空实现伪造跨平台支持。\n\n## Compose 屏幕约束\n\n- 新屏幕使用 Compose；不要新增 XML/View Activity/Fragment。\n- Android Compose 新屏幕采用 UDF/MVI：`@Stable XxxUiState`、`XxxIntent`、`XxxEffect`；ViewModel 直接继承\n  `ViewModel`，私有 `MutableStateFlow` / `MutableSharedFlow(extraBufferCapacity = 16)`，对外只暴露只读\n  Flow，并由单一 `onIntent` 分发用户动作。共享 Feature 的状态宿主按 KMP skill 单独决策。\n- Screen/Content 保持无业务逻辑、无 DAO/网络/存储直连；ViewModel 通过 Gateway/Repository/UseCase\n  访问业务能力。\n- 所有 UiState 与 UI item 数据类标注 `@Stable`。Compose 渲染边界中的集合使用\n  `kotlinx.collections.immutable`；内部计算和数据层不机械替换集合类型。\n- 导航、权限、文件选择和其他宿主动作通过回调或 Effect 处理。新目的地优先由 `MainActivity` 的 Navigation\n  3 图组装。\n- 使用项目的 `AppScaffold`、主题、top bar、dialog/sheet、列表和设置组件。正确处理 edge-to-edge 与\n  predictive back。\n- Activity 仅在遗留 `Intent`/result 兼容确有需要时作为薄宿主保留。\n\n具体文件形态和审查清单由 Compose migration/review skill 维护，本文件不复制完整模板。\n\n## 数据与设置边界\n\n- UI（包括 ViewModel）不得新增 DAO、`appDb.*Dao`、网络客户端或旧偏好 API 直连；历史债务由\n  `verifyConfigArchitecture` 的基线棘轮冻结并逐步下调。\n- 普通设置 Gateway 通过 `update { current -> current.copy(...) }` 修改状态；关联字段在一次\n  `copy(...)` 中原子提交。\n- 不引入 `*SettingsUpdate` 分发类型或 `updateAll`。\n- `ReadStyleMutation`、`ThemePackageSettingsGateway.applyAndAwait`、`ThemeStateTransaction`、\n  `AppUiConfigurationGateway` 等专用 API 保持现有形态。\n- Koin 中 Gateway 接口到 Repository 实现保持显式绑定；不要用构造函数绑定掩盖接口归属。普通\n  Repository/UseCase/ViewModel 可继续使用项目既有 `singleOf` / `viewModelOf` 约定。\n- 新的共享领域契约不得暴露 `Context`、`File`、`Uri`、Room 类型、Android resource id 或 JVM-only\n  stream；用领域值、`ByteArray`/抽象 source-sink、序列化模型或平台接口表达。\n\n## KMP/CMP 迁移纪律\n\n- 先写行为/契约测试和依赖清单，再移动代码；一个 PR 只完成一个可说明的边界变化。\n- 优先抽取稳定模型、纯函数、规则解析契约和 UseCase；网络、数据库、JS 引擎、服务、阅读器渲染最后通过平台适配器处理。\n- `expect/actual` 只用于真正的平台原语。可用普通接口 + DI 表达的能力，不使用 `expect/actual`。\n- 平台实现由 Koin 和应用 composition root 组装。\n- 不因 KMP 目标同时替换数据库、网络、DI、导航和 UI 框架。每次只改变一个主要风险维度。\n- Feature `api/impl` 只有形成真实 Gradle 边界并存在跨 Feature/宿主调用时才创建，不能只建同名文件夹。\n- 首个非 Android 目标是架构证明，不承诺功能齐平；目标顺序由 capability matrix 和产品需求决定。\n\n## 代码生成\n\n- 生成器用于编码已稳定的约定，不用于发明架构。至少有两个手工迁移且通过审查的同类样本后，才能固化模板。\n- 脚手架必须可预览或 dry-run、拒绝覆盖已有文件、输出最小文件集，并在已有 Feature 上采用追加/AST\n  感知修改，禁止覆盖 route graph 或 DI 文件。\n- 生成的可提交源码仍接受普通代码审查；真正的编译期生成物放入 `build/generated`，不得手工编辑或提交。\n- 模板和生成器变更必须有 fixture/snapshot 或编译验证，并与 convention plugin 的模块类型保持一致。\n\n## 重构审查门禁\n\n每个迁移 PR 至少回答：\n\n- 行为基线是什么，哪些测试或手工路径证明没有回退？\n- 依赖边界净变化是什么，是否减少了 Android/JVM 泄漏或历史基线？\n- 新模块/接口是否有真实调用方，能否用更小改动完成？\n- Android 实现与共享契约的错误、线程、取消、序列化语义是否一致？\n- 做了哪些验证，哪些真机、性能、外部服务或其他平台行为尚未验证？\n- CI 是否执行了受影响的共享测试、目标编译或 adapter contract test？\n\n减少历史违规时必须同步下调对应 baseline；门禁不接受“先放宽基线再迁移”。详细分级见 KMP/CMP 现代化计划。\n\n## 构建与验证\n\n开发与 CI 使用 JDK 21。按风险选择最小充分验证：\n\n```powershell\n# Kotlin/Compose 快速编译\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# 当前主验证集\n.\\gradlew.bat testAppDebugUnitTest lintAppDebug verifyConfigArchitecture assembleAppDebug --continue --no-configuration-cache\n\n# 资源、Manifest、生成绑定或打包变化\n.\\gradlew.bat :app:assembleAppDebug\n\n# Release/R8 相关\n.\\gradlew.bat assembleAppRelease\n.\\gradlew.bat assembleAppNoR8\n\n# Web 前端\npnpm --dir modules/web build\n```\n\nKMP 任务名在模块实际创建后才存在；不要假装运行尚未定义的 `commonTest`、目标编译或 API\n检查任务。新增模块时把其真实任务加入 CI，并在交付中列出准确命令。\n\n所有文本改动至少运行 `git diff --check`。构建通过不替代架构边界、行为和真机性能复核。\n\n## 重要项目约束\n\n- 不将 jsoup 升级到 1.16.2 以上；新版行为会影响 `AnalyzeByJSoup.kt` 与 JsoupXpath。\n- 应用代码仅在两处依赖 Hutool：①作为书源 JS 的运行时加密库保留在 classpath（书源直接调用\n  `Packages.cn.hutool.*`）；②`help/crypto/CryptoUtils.kt` 的 base64 **解码**统一走\n  `cn.hutool.core.codec.Base64.decode`，以兼容缺 `=` 填充等宽松输入（Kotlin\n  `kotlin.io.encoding.Base64` 严格填充会导致部分书源取章名/正文回归）。版本固定 5.8.22 勿升级。\n  应用内部加密主体仍用现有 JCA（`javax.crypto`/`java.security`）与 `help/crypto` 路径，KMP 抽取时\n  再通过能力契约替换 JVM API。\n- 代码 namespace 为 `io.legado.app`，Android `applicationId` 为 `io.legato.kazusa`，不要混用。\n- 当前 minSdk 26、target/compile SDK 37；Release 启用 R8 与资源压缩，`noR8` 变体用于排障。\n- Rhino 书源/RSS/TTS 规则、Android 服务、Web 服务和阅读器渲染属于高行为风险平台能力，迁移前必须建立兼容测试或\n  capability 边界。\n- `modules/web` 必须连接应用内 Ktor WebService；开发环境通过 `VITE_API` 指向设备服务地址。\n\n## 交付说明\n\n最终说明应包含：修改的职责范围、关键设计取舍、实际运行的验证与结果、未验证风险。不要把“已生成代码”或“已移动文件”当作完成标准。\n","isInternal":false,"tokens":3948,"sizeBytes":11595},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis is a Material Design 3 fork of [Legado](https://github.com/gedoor/legado). `app/src/main/java/io/legado/app/` uses **Clean Architecture** with three layers:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `:app`, `:modules:book` (epub/TXT parsing, namespace `me.ag2s`), `:modules:rhino` (Rhino JS wrapper, namespace `com.script`). There is also a Vue 3 web frontend in `modules/web/` (pnpm, separate from the Android build).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`MainActivity` holds a single `NavDisplay` with `entryProvider { ... }` defining all composable entries. `Launcher0` through `LauncherW` extend `MainActivity` to provide multiple launcher icon alias entries. Separate activities handle the reader (`ReadBookActivity` — still View-based), book info, source management, replace rules, file manager, QR scanner, etc.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 theme modes (`AppThemeMode` enum) — Dynamic (Monet), 12 named presets, Custom (MaterialKolor seed-color generation), Transparent. `CustomColorScheme` wraps `com.materialkolor` with configurable `PaletteStyle` (TonalSpot, Neutral, Vibrant, Expressive, Rainbow, etc.) and `ColorSpec` (2021 vs 2025).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.claude/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook sources, RSS sources, and HTTP TTS use JavaScript rules. `initRhino()` in `App.kt` registers `NativeBaseSource` wrappers for `BookSource`, `RssSource`, `HttpTTS` (writable JS objects) and `ReadOnlyJavaObject` wrappers for rule entities. Rule parsing logic lives in `help/source/` and `model/analyzeRule/`.\n\n## Important Constraints\n\n- **Do not update jsoup** beyond 1.16.2 — a breaking change in newer versions (see [jsoup#2017](https://github.com/jhy/jsoup/pull/2017)) affects `AnalyzeByJSoup.kt` and the JsoupXpath library\n- Hutool is back on the classpath at 5.8.22 (do not upgrade). Book-source JS calls it via\n  `Packages.cn.hutool.*`; app-internal **base64 decoding** in `help/crypto/CryptoUtils.kt` also\n  routes through `cn.hutool.core.codec.Base64.decode` for lenient input compatibility (Kotlin\n  `kotlin.io.encoding.Base64` is strict about `=` padding). App crypto otherwise uses JCA (\n  `javax.crypto`/`java.security`); new internal tools live in `help/crypto/CryptoUtils.kt`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated in `modules/web/` — a Vue 3 + TypeScript + Vite project for remote bookshelf and source editing. Must connect to the app's built-in HTTP server (started via `WebService` in the main activity settings). Commands:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n","isInternal":false,"tokens":3763,"sizeBytes":15294},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Copilot repository instructions\n\nRead and follow `/AGENTS.md` before changing this repository. It is the source of truth for\nproject-wide constraints, source priority, validation, current Android boundaries, and the KMP/CMP\ndirection.\n\nFor specialized work, also read the matching repository skill:\n\n- `/.agents/skills/legado-compose-migration/SKILL.md` for View/XML to Compose or new Android Compose\n  screens.\n- `/.agents/skills/legado-compose-review/SKILL.md` for Compose architecture and behavior review.\n- `/.agents/skills/legado-kmp-migration/SKILL.md` for modularization, `commonMain`, CMP, platform\n  adapters, migration gates, and scaffolding.\n\nDo not infer current package names, Gradle tasks, dependencies, navigation APIs, or platform support\nfrom examples. Confirm them in source, tests, `settings.gradle`, Gradle build files, and\n`gradle/libs.versions.toml`. The KMP target structure in `docs/dev/kmp-cmp-modernization.md` is a\nstaged proposal, not a description of modules that already exist.\n\nKeep changes surgical, preserve existing workspace edits, run risk-matched verification, and report\nexact commands plus unverified behavior.\n","isInternal":false,"tokens":265,"sizeBytes":1152},{"name":"project-patterns.md","path":".agents/skills/legado-compose-migration/references/project-patterns.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-compose-migration/references/project-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Legado Android Compose Project Patterns\n\nRead this reference while implementing a screen. Confirm every referenced API against the current\ncheckout because the repository is mid-migration.\n\n## Ownership and placement\n\n- Android application module: `:app`.\n- Canonical new Feature package: `io.legado.app.feature.<name>`.\n- `ui/...` is legacy/migration territory. Keep only explicit compatibility owners there after a\n  Feature establishes its canonical package.\n- `MainActivity` owns new in-app Navigation 3 destinations and the root graph.\n- A retained Activity translates stable external/legacy Intent inputs and results; it is not a\n  second presentation or navigation owner.\n- App-level Koin aggregation remains in the host. Use the repository's existing `viewModelOf` or\n  parameterized `viewModel` convention rather than adding a second DI pattern.\n\nA Feature may contain Contract, ViewModel, Route, Screen, components, dialogs, sheets and\npresentation models, but create only the files its behavior needs. Directory shape is not an\nacceptance criterion.\n\n## State and effects\n\nFor a behavior-heavy new screen, the repository expects:\n\n- `@Stable` Compose-facing `UiState` and UI item models;\n- immutable collections at the Compose rendering boundary;\n- private `MutableStateFlow`, exposed as read-only `StateFlow`;\n- when best-effort transient effects are actually needed, private\n  `MutableSharedFlow(extraBufferCapacity = 16)`, exposed as read-only `SharedFlow`;\n- one `onIntent` dispatcher for user actions;\n- host actions expressed as effects or callbacks.\n\n`@Stable` is an assertion, not a magic optimization. Every public property must remain stable and\nchanges observed by Compose. Wrap unstable data only when the wrapper has correct equality and\nmutation semantics. Use measurement/compiler reports before inventing performance abstractions.\n\nKeep source-of-truth business state in the ViewModel. Local `remember`/`rememberSaveable` is\nsuitable\nfor UI affordances and restorable drafts/IDs, not a second copy of repository state. Avoid\nUI-to-ViewModel feedback loops; restore consistency in the reducer or flow that produces the state.\n\nClassify host actions by delivery semantics:\n\n- A user click whose only meaning is UI navigation can call a host callback directly; use a\n  lifecycle-aware rapid-click guard such as `dropUnlessResumed` where the available Lifecycle\n  version supports it.\n- A ViewModel outcome that must survive a missing collector becomes state with an acknowledgement\n  or another explicit durable protocol.\n- Snackbar/toast/haptic feedback that is intentionally best-effort may use the Feature effect\n  stream. Document the loss behavior rather than assuming `extraBufferCapacity` solves it.\n\n## Host boundaries\n\nKeep these at the route/host unless the project already has a tested abstraction:\n\n- navigation and result delivery;\n- permission and Activity Result launchers;\n- file/document pickers;\n- Android framework dialogs and services;\n- Context-dependent clipboard, URI and external-app operations.\n\nReusable Screen functions receive state and semantic callbacks. They do not know an Activity,\nbinding, DAO, application singleton or root back stack.\n\n## Lists, lifecycle and recomposition\n\n- Use stable keys when item identity survives insertion, removal or reorder; use `contentType` when\n  heterogeneous reuse matters.\n- Collect Android route state with `collectAsStateWithLifecycle` unless another lifecycle is\n  deliberate and documented. Apply the same delivery analysis to effect collectors; composition\n  lifetime and host `RESUMED` state are not interchangeable.\n- Key `LaunchedEffect` by the lifetime it represents. Use `rememberUpdatedState` for changing\n  callbacks captured by an effect that should not restart.\n- Derive cheap display values in composition; memoize or move work only when its cost/lifetime\n  warrants it.\n- Prefer immutable Compose-facing collections, while leaving temporary computations and data-layer\n  APIs in their natural collection types.\n- Strong Skipping is enabled by default on modern Kotlin/Compose compiler versions. Unstable\n  parameters are compared by identity, so avoid needless instance churn, but do not add wrappers or\n  `@Stable` solely from intuition. The repository requires the annotation on UI state/item types;\n  those types must actually satisfy its equality and observable-mutation contract.\n\n## Insets and back\n\nTrace which layer owns each inset. For a Material 3 `Scaffold`, inspect its configured\n`contentWindowInsets` and verify that content applies/consumes the provided padding correctly.\nSheets, dialogs, IME and nested scaffolds may need separate treatment. Visual/manual evidence is\nrequired; the presence of `Scaffold`, `safeDrawing` or padding modifiers alone proves nothing.\n\nNavigation 3 integration does not remove the need to verify custom back interception, selection\nmode, unsaved changes and retained Activity entry points. Route back actions through the same owner\nthat decides whether leaving is allowed. Use `PredictiveBackHandler` when gesture progress drives\nUI;\nwhen customizing `NavDisplay` transitions, also supply/test predictive-pop behavior.\n\n## Adaptive layouts and accessibility\n\nBecause this app targets API 37, large-screen orientation, aspect-ratio and resizability\nrestrictions\ncannot be used as a compatibility fallback. Treat the current app window as dynamic across rotation,\nfold/unfold, split-screen and desktop windowing.\n\n- Test compact and expanded widths for new or substantially migrated destinations.\n- Use current window metrics/window size classes for layout decisions; do not branch on a physical\n  device category or assume portrait.\n- Preserve important input/draft/selection state across recreation with the appropriate local\n  saveable state or `SavedStateHandle`, based on the state owner and size.\n- Do not stretch a phone layout indefinitely. Adopt list-detail/supporting panes or adaptive\n  navigation only when the Feature benefits; avoid adding a library merely to satisfy a checklist.\n- Prefer standard interactive components/modifiers. Custom pointer input needs semantic actions,\n  focus/keyboard access and a usable touch target.\n\n## Architecture boundary\n\nNew UI and ViewModels do not add DAO, `appDb`, network-client or old preference access. Use existing\nGateway/Repository/UseCase contracts, or add the smallest real boundary with an actual caller.\n\nFor a strictly UI-only migration, an existing presentation-layer violation may remain to avoid\ncombining architecture and UI rewrites. Freeze rather than duplicate it, document it, and do not\ndescribe the screen as fully modernized until the boundary is corrected.\n\n## Verification selection\n\n- Kotlin-only: `:app:compileAppDebugKotlin`.\n- Resources/manifest/XML/generated binding/package: `:app:assembleAppDebug`.\n- Changed state transitions: focused unit tests with success/failure/cancellation cases as relevant.\n- Navigation or compatibility: exercise the MainActivity route and every retained Intent/result\n  entry.\n- Insets, IME, accessibility and predictive back: device/emulator evidence where risk warrants it.\n- New/substantially changed destinations: representative compact and expanded window checks plus\n  rotation/recreation; add targeted adaptive UI tests when layout branching is meaningful.\n\nUse `AGENTS.md` for the canonical full verification set and exact wrapper command.\n\n## Current official references\n\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting)\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle)\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events)\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n- [Compose side effects](https://developer.android.com/develop/ui/compose/side-effects)\n- [Material Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n- [Adaptive layouts for resizable apps](https://developer.android.com/develop/adaptive-apps/guides/app-orientation-aspect-ratio-resizability)\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n","isInternal":false,"tokens":1676,"sizeBytes":8373},{"name":"SKILL.md","path":".agents/skills/legado-compose-migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-compose-migration/SKILL.md","title":"legado-compose-migration","category":"anthropic-skill","format":"markdown","content":"---\nname: legado-compose-migration\ndescription: Create or migrate a Legado Android screen with Jetpack Compose while preserving behavior and following the repository's Feature ownership, UDF, navigation, DI, inset, and compatibility boundaries. Use for XML/View/RecyclerView/DialogFragment migrations and new Android Compose destinations. Use legado-compose-review instead for review-only requests, and legado-kmp-migration when the change crosses a multiplatform or Gradle boundary.\n---\n\n# Legado Compose Migration\n\n## Purpose\n\nDeliver one working Android UI surface without importing View-era ownership into new Compose code.\nFor migrations, preserve observable behavior before cleanup. For new screens, use the repository's\ncurrent Feature-first and UDF conventions.\n\nBefore editing, read `AGENTS.md`, the relevant part of `docs/dev/feature-first-structure.md`, the\ncurrent implementation and one or two nearby examples that exercise the same behavior. Read\n[references/project-patterns.md](references/project-patterns.md) for repository-specific placement,\nstate and host details; examples are evidence, not templates to copy blindly.\n\n## Decide the shape\n\n- **New destination:** prefer a `MainActivity` Navigation 3 destination. Do not create a standalone\n  Activity without an external-entry or platform lifecycle reason.\n- **Migrated destination:** retain an Activity only as a thin compatibility host when existing\n  callers require stable Android Intent extras/results or a framework contract.\n- **Partial migration:** keep mature View rendering or platform integrations as explicit islands\n  when replacing them would add behavioral risk unrelated to the request.\n- **Multiplatform boundary:** stop treating this as only an Android screen migration and also use\n  `legado-kmp-migration`.\n\n## Workflow\n\n1. Bound the surface and success criteria. Record inputs, navigation/results, empty/loading/error\n   states, actions, dialogs/sheets, persistence, back behavior and external effects that must\n   remain.\n2. Read the smallest complete slice: UI, state owner, host/route, DI, resources, data/use-case\n   calls,\n   and any old implementation needed as a behavior reference.\n3. Choose the lowest state owner that reads and writes each value. Put durable business/render state\n   in `UiState`; keep purely local focus, expansion, animation and draft state in the UI when no\n   higher owner needs it. Route Feature/business actions through the Feature intent type. A direct\n   user action that only requests host navigation may use a host callback. Dialog/sheet visibility\n   is UI state; Android framework dialogs, permissions, file pickers and launches stay in the host.\n4. Keep Screen/Content free of repository, DAO, network, storage and service access. New\n   presentation\n   code uses Repository/Gateway/UseCase boundaries. Preserve an existing violation only when the\n   task is explicitly UI-only, do not spread it, and record the remaining debt.\n5. Wire navigation, DI and compatibility behavior at the host boundary. Keep public extras, result\n   codes and deep-link semantics stable unless the user requested an API change.\n6. Delete XML, adapters, bindings and resources only after references and compatibility callers are\n   gone.\n7. Verify in proportion to risk and report both passed evidence and unverified manual/device paths.\n\n## Project constraints\n\n- New ownership lives under `io.legado.app.feature.<name>`; legacy `ui/...` is a migration source,\n  not the destination for a second implementation.\n- Use ViewModel-owned read-only `StateFlow` and one `onIntent` dispatcher for Feature/business\n  actions, as required by `AGENTS.md`. When a Feature has best-effort transient effects, expose the\n  project's `SharedFlow(extraBufferCapacity = 16)` shape. Buffer capacity does not make emissions\n  durable while no UI is collecting: navigation/result/destructive completion and other\n  consistency-critical ViewModel outcomes must reduce to state or use an explicit acknowledgement\n  protocol instead of relying on an effect stream.\n- Annotate Compose-facing `UiState` and UI item models with `@Stable` only while honoring the\n  annotation's contract: public mutable properties and unobservable mutation make it incorrect.\n  Use immutable collections at the rendering boundary; do not change repository/data collections\n  mechanically.\n- On Android, collect `Flow` UI state with `collectAsStateWithLifecycle` unless a documented\n  lifecycle requirement calls for something else. A `LaunchedEffect` collector is scoped to the\n  composition, not automatically to `STARTED`/`RESUMED`. Long-lived effects must use appropriate\n  keys and `rememberUpdatedState` for changing values that should not restart them.\n- Reuse project theme and components when their behavior fits. A wrapper's existence is not enough\n  reason to use it when semantics, accessibility or host requirements differ.\n- Insets are an ownership decision. Inspect `Scaffold` `contentWindowInsets`, whether content\n  consumes `innerPadding`, nested scaffolds/sheets and IME behavior. Do not assume that using\n  `Scaffold` alone proves edge-to-edge correctness, and do not double-apply system-bar padding.\n- Preserve predictive-back behavior. Use ordinary `BackHandler` only for binary interception; use\n  `PredictiveBackHandler` when custom UI needs gesture progress. If Navigation 3 transitions are\n  customized, define and verify predictive-pop behavior as well as the normal pop transition.\n- The app targets API 37, where orientation, aspect-ratio and resizability restrictions no longer\n  protect layouts on large screens. New or substantially migrated destinations must tolerate\n  compact, medium and expanded resizable windows, rotation and recreation. Base layout decisions on\n  the current app window, not physical-device assumptions; do not add an adaptive library or\n  multi-pane layout unless the screen's behavior benefits from it.\n- Prefer Material/Foundation interactions for their semantics, focus and keyboard behavior. Custom\n  controls and gestures must provide appropriate role/state/action semantics, keyboard/D-pad access\n  and an adequate touch target.\n- Keep localized user-facing text in resources and retain fallback behavior.\n\n## Verification\n\n- Kotlin-only presentation/host changes: run `:app:compileAppDebugKotlin` using the repository's\n  Gradle wrapper syntax for the current shell.\n- Resource, manifest, XML/binding deletion or packaging changes: run `:app:assembleAppDebug`.\n- State/business changes: run focused ViewModel/use-case tests or add a characterization seam.\n- Navigation/Intent/results/insets/back: manually or instrumentedly exercise every retained entry\n  path that compilation cannot prove.\n- New or substantially changed layouts: exercise representative compact and expanded resizable\n  windows, rotation/recreation and IME interaction rather than validating only one portrait phone.\n- Always run `git diff --check`.\n\nUse actual task names from this checkout; do not invent tasks from this skill.\n","frontmatter":{"name":"legado-compose-migration","description":"Create or migrate a Legado Android screen with Jetpack Compose while preserving behavior and following the repository's Feature ownership, UDF, navigation, DI, inset, and compatibility boundaries. Use for XML/View/RecyclerView/DialogFragment migrations and new Android Compose destinations. Use legado-compose-review instead for review-only requests, and legado-kmp-migration when the change crosses a multiplatform or Gradle boundary."},"isInternal":false,"tokens":1418,"sizeBytes":7030},{"name":"review-checklist.md","path":".agents/skills/legado-compose-review/references/review-checklist.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-compose-review/references/review-checklist.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Legado Compose Review Checklist\n\nUse only the sections relevant to the scoped surface.\n\n## Context\n\n- Confirm the canonical Feature owner and whether any legacy `ui/...` file is a compatibility owner.\n- Read Screen/Content, ViewModel/state contract, route/Activity, DI and directly used domain/data\n  contracts.\n- Read old XML/View/adapters only when needed for parity or a live caller.\n- Identify all entry points: MainActivity route, Intent/deep link, notification/widget/service or\n  Activity Result.\n\n## Behavior and state\n\n- Is there one durable state owner, or can ViewModel, Activity, `remember`, adapter and repository\n  copies diverge?\n- Is each state hoisted only to the lowest owner that needs to read and write it? Local expansion,\n  focus or animation state need not be forced into a ViewModel; business state must not be copied\n  into local Compose state.\n- Can loading, empty, error, selection, query, sorting and dialog/sheet states be reconstructed\n  after\n  recreation where required?\n- Are one-shot effects consumed once without becoming persistent replayable flags?\n- Can a ViewModel-produced effect be emitted while the route has no active collector? If losing it\n  would leave navigation, results or durable business state inconsistent, model/acknowledge it as\n  state rather than relying on `SharedFlow` buffering.\n- Are success, failure, cancellation and stale-response ordering handled?\n- Do `LaunchedEffect` keys match the intended lifetime and avoid duplicate loads/navigation/toasts?\n- Is `Flow`-backed Android UI state collected with `collectAsStateWithLifecycle`, or is there a\n  documented lifecycle reason for another collector?\n- Are mutable collections/entities exposed in a way that invalidates a claimed `@Stable` contract?\n- With Strong Skipping, are unstable parameters recreated on each update and compared by identity?\n  Ask for compiler/measurement evidence before treating ordinary recomposition as a defect.\n- Do lazy items use stable identity where insert/remove/reorder can attach state to the wrong row?\n\n## Architecture and ownership\n\n- Screen/Content renders state and emits semantic actions; it does not access DAO, repository,\n  network, storage, service or global application state.\n- New ViewModel code uses Gateway/Repository/UseCase boundaries and does not add DAO or old global\n  access.\n- Host owns navigation, framework launchers, permissions, Context/URI work and root lifecycle.\n- A retained Activity translates compatibility inputs/results without becoming a second business or\n  navigation owner.\n- Feature code does not depend on another Feature implementation.\n- New files do not extend a legacy package after a canonical Feature owner exists.\n\n## Navigation and compatibility\n\n- New in-app destinations are registered in the current MainActivity graph unless an external-entry\n  requirement justifies an Activity.\n- Existing extras, result codes, deep links and callers preserve semantics.\n- Multiple entry paths initialize equivalent state and converge on the same owner.\n- Nested UI requests navigation through callbacks/effects rather than mutating the root stack.\n- Pure UI navigation can call a host callback directly. Check rapid taps and lifecycle state; use a\n  lifecycle-aware guard such as `dropUnlessResumed` where supported rather than inventing a\n  ViewModel effect solely to move the navigation call.\n- Back interception covers real selection/unsaved state without breaking predictive back or host\n  result delivery.\n- Custom gesture-progress UI uses `PredictiveBackHandler`; customized Navigation 3 pop transitions\n  also define/verify predictive-pop behavior.\n\n## UI, Insets and accessibility\n\n- Trace `Scaffold` `contentWindowInsets`, inner padding consumption, nested scaffolds/sheets, system\n  bars and IME. Check for both overlap and double padding.\n- User-facing strings retain localization and fallback behavior.\n- Project components/theme are used when their semantics fit; wrappers are not applied mechanically.\n- Touch targets, focus order, semantics/content descriptions and keyboard behavior fit the control.\n- Custom pointer input has an accessible semantic action and keyboard/D-pad alternative; standard\n  Material/Foundation interactions are preferred when their behavior fits.\n- Lists/images preserve existing cache, loading and accessibility behavior where relevant.\n- Removed XML/binding/menu resources have no remaining references.\n\n## Verification and output\n\nTie each finding to evidence and the smallest fix. Recommend only checks that can observe the risk:\n\n- compilation for Kotlin wiring;\n- assemble for resources, manifest, XML/binding and packaging;\n- focused unit tests for reducers/ViewModels/use cases;\n- route plus retained Intent/result path checks for navigation compatibility;\n- device/emulator checks for Insets, IME, accessibility, predictive back and rendering performance.\n\nUse P0/P1/P2/P3 from the skill. If there are no findings, explicitly list what was not executed or\nobserved.\n","isInternal":false,"tokens":976,"sizeBytes":5003},{"name":"SKILL.md","path":".agents/skills/legado-compose-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-compose-review/SKILL.md","title":"legado-compose-review","category":"anthropic-skill","format":"markdown","content":"---\nname: legado-compose-review\ndescription: Review existing Legado Android Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, and compatibility hosts for concrete behavior, lifecycle, state ownership, architecture, inset, accessibility, and maintainability risks. Use for review or audit requests. Do not rewrite code unless fixes are requested; use legado-compose-migration for implementation and legado-kmp-migration for multiplatform or Gradle-boundary findings.\n---\n\n# Legado Compose Review\n\n## Purpose\n\nFind concrete defects and risky drift before proposing rewrites. Judge the code against current\nrepository behavior and constraints, not a preferred template.\n\nRead `AGENTS.md`, the scoped files and\n[references/review-checklist.md](references/review-checklist.md). Consult an old View implementation\nonly when it remains a caller or behavior baseline.\n\n## Review workflow\n\n1. Define the exact surface and whether the user asked for review only or fixes. Review-only work\n   does not authorize edits.\n2. Inspect the smallest complete behavior slice: Screen, state owner/contract, route/host, DI,\n   relevant repositories/use cases, resources and compatibility entry points.\n3. Trace state and effects end to end. Look for divergent owners, replayed one-shot work, lifecycle\n   mistakes, lost events, unstable identity and error/cancellation paths before style issues.\n4. Trace navigation/results, Insets/IME, back, accessibility and platform launchers through the real\n   host. Do not infer correctness from the presence of a particular API.\n5. Report findings first with tight line references, concrete impact and the smallest credible fix.\n   Separate compatibility-preserving fixes from optional redesign.\n6. If no finding exists, say so and list the important unverified runtime/device paths.\n\n## Severity\n\n- **P0/P1:** crash, data/behavior loss, broken navigation/result/entry compatibility, unsafe\n  lifecycle or concurrency, repeated destructive effect, inaccessible/obscured critical content.\n- **P2:** state ownership that can diverge, business/data access in UI, platform leakage, incorrect\n  stability contract, recurring recomposition/performance issue, or architecture drift with a real\n  maintenance cost.\n- **P3:** localized convention, naming, testability or cleanup issue without demonstrated behavior\n  or structural impact.\n\nDo not promote a missing annotation, different file split or wrapper choice to P2 without explaining\nthe observable or architectural consequence.\n\n## Boundaries\n\n- Do not require every screen to have the same number of files or abstractions.\n- Allow thin compatibility Activities and mature View/platform islands when they have a current\n  caller and one clear owner.\n- New Compose presentation must not add DAO, network, storage or service access; report existing\n  debt without turning a review into an unauthorized domain rewrite.\n- Treat `@Stable` as a contract that can itself be wrong, not as proof of stability or performance.\n- Strong Skipping is enabled by default with modern Kotlin/Compose compiler versions, so an\n  unstable parameter is not automatically a performance defect. Check identity churn, actual\n  recomposition evidence and compiler/benchmark data. A missing project-required annotation is P3\n  unless it causes a concrete architectural or runtime issue; an incorrect `@Stable` promise can be\n  P2 because it can suppress required updates.\n- Android `Flow` state should normally be collected with `collectAsStateWithLifecycle`. A\n  composition-scoped collector is not automatically lifecycle-aware merely because it uses\n  `LaunchedEffect`.\n- Review each ViewModel-originated `SharedFlow` effect by delivery semantics. Buffer capacity does\n  not make an event durable while no UI is collecting. Navigation, result delivery, payments,\n  destructive completion and other consistency-critical outcomes should normally reduce to state\n  or use an explicit acknowledgement protocol; best-effort transient feedback may remain an effect.\n- UI-only behavior should stay at the lowest UI owner. A direct user navigation callback handled by\n  the host is valid and often preferable to a ViewModel round trip; guard rapid/replayed navigation\n  with lifecycle-aware mechanisms such as `dropUnlessResumed` when the available Lifecycle version\n  supports it.\n- Insets and predictive back require ownership and runtime-path analysis; `Scaffold`, padding APIs,\n  Navigation 3 or `BackHandler` alone do not prove correctness.\n- Use `PredictiveBackHandler` when custom UI needs gesture progress; use ordinary `BackHandler` only\n  when binary interception is sufficient. When Navigation 3 transitions are customized, inspect\n  `predictivePopTransitionSpec` as well as normal pop behavior.\n- Reusing a project component is preferred only when its behavior, semantics and accessibility fit.\n- Prefer Material/Foundation interaction APIs because they provide semantics, focus and input\n  behavior. Custom gestures/components must preserve role, state/action semantics, keyboard/D-pad\n  access and an adequate touch target.\n\n## Current official baseline\n\nWhen library behavior matters, verify it against the repository version and current official docs:\n\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n  and [stability contracts](https://developer.android.com/develop/ui/compose/lifecycle).\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle).\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting).\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events).\n- [Material 3 Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n  and [Compose Insets](https://developer.android.com/develop/ui/compose/system/insets-ui).\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n  and [Navigation 3 transitions](https://developer.android.com/guide/navigation/navigation-3/animate-destinations).\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n  and [semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics).\n\nWhen implementation is requested, also read the migration skill's project patterns.\n","frontmatter":{"name":"legado-compose-review","description":"Review existing Legado Android Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, and compatibility hosts for concrete behavior, lifecycle, state ownership, architecture, inset, accessibility, and maintainability risks. Use for review or audit requests. Do not rewrite code unless fixes are requested; use legado-compose-migration for implementation and legado-kmp-migration for multiplatform or Gradle-boundary findings."},"isInternal":false,"tokens":1283,"sizeBytes":6410},{"name":"openai.yaml","path":".agents/skills/legado-kmp-migration/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-kmp-migration/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"interface:\n  display_name: \"Legado KMP-first Migration\"\n  short_description: \"Plan shared KMP logic and independent native or Compose renderers\"\n  default_prompt: \"Use $legado-kmp-migration to plan a reviewable KMP-first slice with explicit host, renderer, interop, and verification boundaries.\"\n","isInternal":false,"tokens":68,"sizeBytes":296},{"name":"renderer-host-strategy.md","path":".agents/skills/legado-kmp-migration/references/renderer-host-strategy.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-kmp-migration/references/renderer-host-strategy.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Renderer and Host Strategy\n\nRead this reference when a Feature may use more than one UI stack or when a non-Kotlin host consumes\nshared Kotlin behavior.\n\n## Separate three decisions\n\n1. **Shared behavior:** domain, repositories, use cases, state snapshots, commands and reducers.\n2. **Host integration:** Gradle/JVM, Apple framework, Windows DLL/C ABI, or process/IPC.\n3. **Renderer:** Android Material/Miuix, Compose Material/Fluent, WinUI 3, SwiftUI/UIKit, or a\n   specialized platform island.\n\nDo not infer one decision from another. A KMP core does not require CMP, and Compose on one host\ndoes\nnot require every host to share the same Screen.\n\n## Renderer-neutral presentation\n\nPrefer immutable state snapshots, stable IDs, commands/intents, effects, domain values and explicit\ncapability/error states. Keep Composable lambdas, `Modifier`, Material/Fluent types,\n`StringResource`, icons/painters, navigation UI objects, Android Context, Swift/WinRT objects, Room\nentities and DI containers out of the boundary.\n\n`@Stable` belongs to a Compose-facing contract or adapter. If WinUI 3 or SwiftUI also consumes the\nstate, keep the pure state free of renderer annotations and adapt it at the Compose boundary.\n\n## WinUI 3 choices\n\nWinUI 3 cannot consume a Kotlin/JVM Gradle module like a Kotlin host. Choose the bridge explicitly.\n\n### Kotlin/Native DLL and C ABI\n\nUse a `mingwX64` shared library when in-process calls and one packaged process matter, and only when\nthe required dependency closure supports the target.\n\n- Export a small facade, not repositories, Flow, sealed hierarchies or generic Kotlin APIs.\n- Use opaque handles with explicit create/dispose.\n- Define ownership for every returned string or buffer.\n- Map async work to request IDs plus callbacks/polling; define callback thread and cancellation.\n- Version the ABI and compile a native consumer in CI.\n- Do not force JVM-only Room, Rhino or jsoup dependencies into the DLL; split a native-compatible\n  core or choose IPC for those capabilities.\n\n### JVM sidecar and IPC\n\nKeep the existing JVM data/runtime stack in a separately packaged process and expose a versioned\nlocal protocol when reuse is more important than in-process integration.\n\n- Define protocol DTOs and version negotiation.\n- Define startup, readiness, shutdown, crash recovery and upgrade behavior.\n- Restrict local access; do not unintentionally expose an unauthenticated network service.\n- Propagate cancellation and structured errors.\n- Package and test both processes as one product.\n\nUse one bounded read/write Feature to compare cold start, call latency, state mapping, database and\nruntime reuse, memory ownership, crash isolation, installer complexity and test ergonomics before\nchoosing the product-wide bridge.\n\n## iOS native renderer\n\nExport a deliberately small Apple framework facade. SwiftUI may observe a tested shared state host\nor own an `ObservableObject` that calls shared repositories/use cases. Verify Swift names,\noptionality, collections, async/Flow bridging, lifecycle, cancellation and memory ownership with a\nSwift consumer test.\n\n## Selective CMP\n\nShare a CMP Screen only when real hosts intentionally share its visual and interaction model.\nResources and design-system components belong to that renderer. If Desktop uses WinUI 3 and iOS\nuses SwiftUI, Android Compose can remain an Android renderer even when its code is technically\nportable.\n","isInternal":false,"tokens":712,"sizeBytes":3408},{"name":"slice-checklist.md","path":".agents/skills/legado-kmp-migration/references/slice-checklist.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-kmp-migration/references/slice-checklist.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# KMP Slice Checklist\n\nUse this reference for implementation plans, extraction work, scaffolding or reviews. The default\npath is renderer-neutral KMP. CMP checks apply only after a shared Compose renderer has been chosen;\nthey are not instructions to convert every Feature or host to Compose.\n\n## Before change\n\n- Scope names exact files/packages, current callers, one boundary and one rollback point.\n- Existing behavior is captured by tests or a written parity list.\n- The target owner, source set and consumers are real rather than copied from the target diagram.\n- Dependencies are classified as common-ready, contract-needed, renderer-specific,\n  platform-island or unknown.\n- Actual Gradle target and verification task names have been inspected.\n\n## Contract quality\n\n- Public types express domain values instead of Android/JVM/storage/rendering details.\n- Error, cancellation, threading, transaction, ordering, serialization and ownership semantics are\n  explicit where they cross the boundary.\n- Capability absence is visible to callers.\n- An ordinary interface is used unless `expect/actual` provides a concrete static benefit.\n- Every abstraction has a real caller and removes a measurable dependency.\n- Native or IPC exports are smaller and more stable than the internal Kotlin API.\n\n## Module and source-set graph\n\n- The app host owns implementation aggregation, DI, navigation/window lifecycle and packaging.\n- Core does not import Feature; Feature does not import another Feature implementation.\n- Shared modules do not depend on platform implementations or renderer modules.\n- Gradle `api` exposure is intentional; otherwise use `implementation`.\n- Package colocation, Android module extraction, KMP conversion and renderer sharing remain separate\n  changes unless evidence requires combining them.\n- `commonMain` is compiled by at least one non-Android target before being called shared.\n- JVM+Android-only sharing has an honest owner and is not mislabeled as common.\n- Platform files live in the narrowest relevant source set.\n- Android services, Context/URI/resources and notifications remain Android-side.\n- Rhino/JS behavior remains behind a capability boundary until another target has a compatible,\n  tested implementation.\n\n## Renderer and host checks\n\n- Presentation contracts contain no Compose, Material/Miuix, Fluent, WinUI, SwiftUI or platform\n  resource types.\n- A renderer owns its resources, accessibility, layout and navigation presentation.\n- Shared Compose UI emits semantic callbacks/effects; the host owns platform launchers.\n- A CMP Screen has at least two intentional consumers or another documented product reason.\n- WinUI/iOS bridges define versioning, lifecycle, cancellation, error and memory ownership.\n- A sidecar protocol defines startup, readiness, shutdown, crash recovery, upgrade and local-access\n  behavior.\n\n## Gates\n\n- Affected Android unit/lint/architecture/package gates pass.\n- Common tests and actual metadata/target compile tasks pass.\n- Capability status distinguishes compile, contract-test, smoke, package and release-ready.\n- Adapter tests cover success, failure and cancellation where relevant.\n- Serialization/database changes include backward/forward or migration evidence.\n- Reader/rule/service changes include parity and, where relevant, real-device performance evidence.\n- Reduced historical violations lower their baseline in the same change.\n- `git diff --check` passes.\n\n## Scaffolding\n\n- At least two accepted manual examples prove a convention before it is generated.\n- Dry-run is available and default execution refuses overwrites.\n- Existing graph/DI files are amended safely rather than replaced wholesale.\n- Only necessary files are generated; no empty layers or speculative targets.\n- Generator behavior has fixture, snapshot or compilation evidence.\n\n## Review output\n\nFor each finding give a tight file/line reference, concrete impact and smallest credible fix:\n\n- P0/P1: behavior/data loss, incompatible rule/storage/ABI semantics, broken target, lifecycle,\n  thread, cancellation, native ownership or packaging defects.\n- P2: illegal dependency, renderer/platform leakage, false capability, state duplication, baseline\n  relaxation or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf no issue is found, state the remaining unverified host, device, package, interop and performance\nrisks.\n","isInternal":false,"tokens":843,"sizeBytes":4415},{"name":"SKILL.md","path":".agents/skills/legado-kmp-migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.agents/skills/legado-kmp-migration/SKILL.md","title":"legado-kmp-migration","category":"anthropic-skill","format":"markdown","content":"---\nname: legado-kmp-migration\ndescription: Plan, implement, or review Legado KMP-first modularization across shared domain, data, and presentation code with independently chosen Android, Desktop, Windows-native, and optional iOS renderers. Use for Gradle boundaries, commonMain extraction, platform capabilities, native or process bridges, selective CMP, target gates, and legacy-owner removal. Do not use for an Android-only View-to-Compose rewrite unless it also changes a multiplatform or Gradle boundary.\n---\n\n# Legado KMP-first Migration\n\n## Purpose\n\nMove one verified responsibility toward a reusable KMP boundary without weakening Android behavior\nor forcing every host to use Compose. Optimize for shared behavior and explicit platform contracts,\nnot maximum shared UI percentage.\n\nValid renderers include Android Compose/View islands, Compose Desktop with Material or Fluent,\nWindows-native WinUI 3, and optional SwiftUI/UIKit. CMP is one renderer technology, not the\narchitecture root.\n\nBefore acting, read repository `AGENTS.md`, `docs/dev/kmp-cmp-modernization.md`, and the relevant\npart of `docs/dev/feature-first-structure.md`.\n\nRead supporting references only when applicable:\n\n- Implementation or review: [references/slice-checklist.md](references/slice-checklist.md).\n- Multiple renderers, WinUI 3, SwiftUI/UIKit, native export or IPC:\n  [references/renderer-host-strategy.md](references/renderer-host-strategy.md).\n\n## Choose the work mode\n\n- **Architecture/plan:** inventory current dependencies and consumers, then choose the sharing\n  boundary, renderer strategy, targets, gates and rollback point.\n- **Pure KMP extraction:** move stable models, rules, ports, use cases, reducers or stores into\n  Compose-free `commonMain`.\n- **Data/runtime extraction:** separate domain contracts from storage, network, files and rule\n  engines while retaining compatibility semantics under contract tests.\n- **Renderer boundary:** split renderer-neutral presentation from Material/Miuix, Fluent, WinUI 3,\n  SwiftUI/UIKit or specialized reader UI.\n- **Selective CMP:** share a Compose Screen/resource set only when real hosts deliberately choose\n  the same renderer and interaction model.\n- **Native host bridge:** expose a small, versioned API through an Apple framework, Windows DLL/C\n  ABI, or process/IPC boundary; do not export the internal Kotlin object graph.\n- **Build logic:** prove a target/convention/gate change with one representative module before\n  broader rollout.\n- **Review:** report findings and unverified targets first; edit only when fixes or plans were\n  requested.\n\n## Decisions required before implementation\n\n1. **What is shared?**\n    - Default candidates are domain values, business rules, repository ports, use cases,\n      serialization models, state snapshots, commands and reducers.\n    - Screens, design-system components, navigation renderers, platform ViewModel owners, resource\n      handles, lifecycle and OS integration are not shared by default.\n    - A pure presentation API must not expose Compose annotations/types, Material/Fluent types,\n      Android resources, Koin, Room, or platform SDK objects.\n\n2. **Who renders it?**\n    - Name each committed host and renderer separately; “Desktop” is not a renderer.\n    - Different renderers may consume the same presentation contract while owning distinct layouts,\n      resources, navigation and accessibility.\n    - Do not invent universal UI wrappers across unrelated design systems. Share semantic values\n      only\n      when their meaning is genuinely common.\n\n3. **How does a non-Kotlin host consume it?**\n    - JVM Compose hosts can depend on KMP/JVM modules directly.\n    - SwiftUI/UIKit needs a deliberately exported Apple framework facade.\n    - WinUI 3 needs either a Kotlin/Native `mingwX64` DLL with a narrow C ABI or a separately\n      packaged\n      JVM process reached through a versioned local IPC protocol.\n    - Keep rich Kotlin types internal. Define DTO/state snapshots, commands, errors, cancellation,\n      callback threads, allocation ownership and disposal at the bridge.\n\n4. **What evidence supports the claim?**\n    - Distinguish metadata/compile, contract-test, host smoke, package and release-ready evidence.\n    - Android plus JVM compilation does not prove iOS or Windows Native readiness.\n    - Do not document a module, target, task or host as current until it exists in this repository.\n\n## Workflow\n\n1. Bound one slice: exact files, callers, behavior, intended owner/source set, host impact and\n   rollback point.\n2. Classify dependencies as `common-ready`, `contract-needed`, `renderer-specific`,\n   `platform-island`, or `unknown`. Imports are only a first pass; compile every claimed target.\n3. Establish characterization or contract tests before moving behavior. Preserve storage, script\n   ABI, serialization, error, cancellation, ordering and threading semantics.\n4. Choose the smallest seam. Prefer interfaces plus constructor injection; use `expect/actual` only\n   for true platform primitives or a documented case where injection cannot provide the boundary.\n5. Implement additively, migrate a bounded caller set, then delete the old owner when no callers\n   remain. Do not keep internal `Help/Utils/Base/Provider` facades for import compatibility.\n6. Run actual repository gates and target tasks. Never infer task support from the target design.\n7. Report behavior evidence, dependency/baseline delta, capability changes, exact commands,\n   rollback path and unverified hosts/devices.\n\n## Boundary rules\n\n- Pure KMP domain/presentation does not depend on Compose, AndroidX ViewModel, Room entities/DAO,\n  Koin, platform resources, `File`/URI, JVM-only libraries or renderer types.\n- Renderer modules may depend on presentation; presentation never depends on a renderer.\n- Each host owns its composition root, root navigation/window lifecycle, platform effects,\n  implementation selection and packaging.\n- Unsupported capabilities are explicit; a successful no-op is not an implementation.\n- Feature-to-Feature implementation dependencies remain forbidden. Create `api/impl` only for a\n  real Gradle consumer; split renderers by responsibility instead of generic layering names.\n- Do not change storage, network, DI, navigation and UI technology in one slice.\n- Historical baselines only decrease. A new source set starts at zero and cannot justify raising a\n  baseline.\n\n## Verification minimums\n\n- Always retain the affected Android G0 gates.\n- Pure KMP: common tests plus every claimed target compile.\n- Data/runtime: adapter contracts and compatibility checks proportional to storage, serialization,\n  cancellation and error risks.\n- CMP renderer: target compile plus rendered UI/semantics smoke and runtime dependency alignment.\n- WinUI 3 DLL: native link, generated API review, consumer smoke, memory/disposal/error/thread tests\n  and package loading.\n- WinUI 3 IPC: protocol compatibility, startup/shutdown/reconnect, local-access policy,\n  cancellation and installer smoke.\n- iOS native UI: framework export plus a Swift consumer compile/observation/lifecycle smoke.\n- Always run `git diff --check`; use a clean rebuild after cross-module moves.\n\n## Review output\n\nList findings by impact with tight file/line evidence and the smallest credible fix:\n\n- P0/P1: behavior or data loss, ABI/storage incompatibility, broken target, lifecycle/thread/\n  cancellation defect, or package/runtime failure.\n- P2: renderer leakage into presentation, illegal dependency, false capability, untested bridge,\n  state duplication, baseline relaxation, or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf there are no findings, state which host, package, device, renderer, interop and performance paths\nremain unverified.\n","frontmatter":{"name":"legado-kmp-migration","description":"Plan, implement, or review Legado KMP-first modularization across shared domain, data, and presentation code with independently chosen Android, Desktop, Windows-native, and optional iOS renderers. Use for Gradle boundaries, commonMain extraction, platform capabilities, native or process bridges, selective CMP, target gates, and legacy-owner removal. Do not use for an Android-only View-to-Compose rewrite unless it also changes a multiplatform or Gradle boundary."},"isInternal":false,"tokens":1586,"sizeBytes":7800},{"name":"openai.yaml","path":".codex/skills/legado-compose-migration/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.codex/skills/legado-compose-migration/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"interface:\n  display_name: \"Legado Compose Migration\"\n  short_description: \"Guide Legado Compose screens and migration\"\n  default_prompt: \"Use $legado-compose-migration to create or migrate a Legado Android Compose screen with MainActivity navigation and standard architecture.\"\n","isInternal":false,"tokens":57,"sizeBytes":279},{"name":"project-patterns.md","path":".codex/skills/legado-compose-migration/references/project-patterns.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.codex/skills/legado-compose-migration/references/project-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Legado Compose Migration Project Patterns\n\n## Existing Shape\n\n- App module: `app`.\n- Main source root: `app/src/main/java/io/legado/app`.\n- UI package: `ui/...`, usually grouped by feature.\n- Data package: `data/dao`, `data/entities`, `data/repository`.\n- Domain package: `domain/gateway`, `domain/model`, `domain/repository`, `domain/usecase`.\n- DI: `di/appModule.kt`, using Koin `singleOf`, `viewModelOf`, and parameterized `viewModel { ... }`.\n- Main navigation: `ui/main/MainActivity.kt`, using Navigation3 `NavKey`, `rememberNavBackStack`, `entryProvider`, and `NavDisplay`, plus legacy route extras such as `EXTRA_START_ROUTE`.\n- Base Compose host: `base/BaseComposeActivity.kt`, which wraps `Content()` in `AppTheme`, configures system bars, locale/font, background image, and LiveBus recreation events.\n- Compose dependencies are already enabled in `app/build.gradle.kts`; do not add new UI libraries unless the target screen truly requires one.\n- `kotlinx.collections.immutable` is available for Compose-facing screen state. Prefer `ImmutableList`, `ImmutableSet`, and `ImmutableMap` at `UiState` boundaries when collection state is passed to composables and changes often.\n\n## Files to Inspect Before Migrating\n\nInspect these as local examples, not as APIs to copy blindly:\n\n- `ui/book/info/BookInfoActivity.kt`\n- `ui/book/info/BookInfoContract.kt`\n- `ui/book/info/BookInfoViewModel.kt`\n- `ui/book/info/BookInfoScreen.kt`\n- `ui/book/search/SearchContract.kt`\n- `ui/book/search/SearchViewModel.kt`\n- `ui/book/search/SearchScreen.kt`\n- `ui/main/MainActivity.kt`\n- `ui/main/MainScreen.kt`\n- `ui/widget/components/AppScaffold.kt`\n- `ui/widget/components/list/ListScaffold.kt`\n- `ui/theme/LegadoTheme.kt`\n- `ui/theme/AppTheme.kt`\n\n## Recommended Feature Layout\n\nFor a migrated feature, prefer colocated files:\n\n```text\nui/<area>/<feature>/\n  FeatureActivity.kt       # only for legacy Intent compatibility or full-screen migration host\n  FeatureContract.kt\n  FeatureViewModel.kt\n  FeatureScreen.kt\n  FeatureSheets.kt        # only if sheets are substantial\n  FeatureDialogs.kt       # only if dialogs are substantial\n```\n\nUse the smallest set of files. Do not split files just to match this shape.\n\n## MVI and UDF Rules\n\nUse this shape for behavior-heavy screens:\n\n```kotlin\ndata class FeatureUiState(\n    val isLoading: Boolean = false,\n    val items: List<Item> = emptyList(),\n    val dialog: FeatureDialog? = null,\n    val sheet: FeatureSheet = FeatureSheet.None,\n)\n\nsealed interface FeatureIntent {\n    data object BackPressed : FeatureIntent\n    data object Refresh : FeatureIntent\n    data class ItemClick(val id: Long) : FeatureIntent\n}\n\nsealed interface FeatureEffect {\n    data object Finish : FeatureEffect\n    data class OpenDetail(val id: Long) : FeatureEffect\n}\n```\n\nViewModel rules:\n\n- Keep `_uiState = MutableStateFlow(FeatureUiState())` private and expose `uiState = _uiState.asStateFlow()`.\n- Keep `_effects = MutableSharedFlow<FeatureEffect>(extraBufferCapacity = 8)` private and expose `effects = _effects.asSharedFlow()`.\n- Prefer `fun onIntent(intent: FeatureIntent)` as the UI entry point.\n- Treat `FeatureIntent` as an MVI/user-action type. When Android `Intent` launch/extras are also involved, name variables and explanations clearly enough that the two concepts are not confused.\n- Use `_uiState.update { it.copy(...) }` for state changes.\n- Emit one-shot work through effects, not booleans in state.\n- Keep cached current entities private in the ViewModel when existing project behavior needs mutation or incremental sync, but publish immutable render state.\n- For collection-heavy `UiState`, convert DAO/repository `List`/`Set`/`Map` values to `kotlinx.collections.immutable` at the ViewModel/UI-state boundary with `toImmutableList()`, `toImmutableSet()`, or `toImmutableMap()`. Do not force repository, DAO, or domain APIs to use persistent collections unless the domain contract truly benefits.\n- Keep internal flow pipelines free to use normal Kotlin collections for sorting, grouping, and persistence work; the immutable collection rule is primarily for values exposed to Compose.\n- Use existing `BaseViewModel.execute { ... }.onSuccess { ... }.onError { ... }` when the surrounding ViewModel already uses that pattern.\n\nActivity rules:\n\n- Prefer `MainActivity` as the owner of Compose-first navigation. Add a Navigation3 route/key and `entryProvider` entry there for new destinations.\n- Extend `BaseComposeActivity` for migrated Activity screens only when a standalone Activity remains necessary.\n- Keep a legacy Activity for a migrated screen only when unreworked View code still calls it through `Intent`; in that case, make the Activity a thin compatibility host that translates extras/results and delegates to Compose state/effects.\n- Keep Activity Result launchers, file openers, clipboard, permission APIs, Android DialogFragments, and framework navigation in the Activity.\n- In `Content()`, collect state with `collectAsStateWithLifecycle().value`.\n- Collect effects in `LaunchedEffect(Unit) { viewModel.effects.collectLatest { ... } }`.\n- Pass only `state` and callbacks into the screen.\n\nNavigation rules:\n\n- For new Compose-first destinations, add the route to `MainActivity` instead of adding a new Activity.\n- Follow `MainActivity`'s current route style: serializable `NavKey` routes for in-process navigation and launcher `Intent` helper methods only when external or legacy callers need a stable entry point.\n- Keep legacy extra names stable when replacing an old Activity entry point.\n- When a compatibility Activity is retained, let it translate old `Intent` inputs/results; do not make it the source of truth for navigation or business behavior.\n\nScreen rules:\n\n- Make `FeatureScreen(state, onIntent, ...)` stateless for business state.\n- Use `remember` and `rememberSaveable` only for local UI affordances such as menu expansion, scroll state, transient animation state, and text field drafts when committing through an intent.\n- Use `rememberSaveable` for user-driven transient targets that should survive recreation, such as a delete-confirmation item ID or URL. Store only the stable ID in saved state and resolve the current entity from `UiState`.\n- Use `BackHandler` to route back actions through `FeatureIntent.BackPressed` when ViewModel decisions matter.\n- Keep expensive derived values in `remember(key)` or ViewModel state when they depend on repository data.\n- Prefer `LazyColumn`, `LazyRow`, stable keys, and existing fast-scroller/list components for adapter migrations. For heterogeneous lazy lists or grids, provide `contentType` in addition to `key`.\n- In long-lived `LaunchedEffect` collectors, wrap changing callbacks, `Context`-dependent operations, or lambdas from parents with `rememberUpdatedState` when the effect should not restart.\n- Hoist state only to the lowest common owner that reads and writes it. Move branch-only ViewModel lookup and Flow collection into the branch or a small child composable instead of collecting at a high-level route.\n- Avoid UI-state feedback loops such as `LaunchedEffect(uiState.items) { viewModel.pruneSelection(...) }`. Prefer deriving consistency inside the ViewModel with `combine(...)`, or reduce the state as part of the flow that produces the data.\n\n## Current Compose Performance Notes\n\n- Kotlin 2.x enables strong skipping by default, but ordinary Kotlin `List`, `Set`, and `Map` are still unstable to Compose. This means high-frequency screen state with normal collections can still widen recomposition work.\n- Use immutable collections for render state that crosses into Compose. Do not mechanically replace every temporary collection, Room query result, or internal mutable accumulator.\n- If data entities come from modules where the Compose compiler does not run, consider a stable UI model wrapper when the entity is passed deeply through composables and causes measurable recomposition cost.\n- Treat `SnapshotStateList` / `SnapshotStateMap` as UI-owned mutable state, not as a default ViewModel `UiState` transport type.\n\n## Clean Architecture Boundaries\n\nThis project is mid-migration, so use Clean Architecture pragmatically. For newly created screens, default to standard modern Android architecture and avoid mixed legacy patterns unless they are needed to interoperate with View screens that have not been rewritten.\n\n- For new screens, keep UI, presentation, domain, and data boundaries explicit: Compose renders state, ViewModel owns state and intent handling, usecases hold reusable business actions, repositories mediate data access.\n- For new screens, do not place business rules in Activity/Fragment or composables.\n- For new screens, do not read DAOs directly from UI/presentation unless the project already has no reasonable repository/usecase boundary and adding one would be disproportionate.\n- For migrations, use existing DAOs/repositories directly if the old ViewModel already does and the migration is UI-only.\n- Prefer existing `domain/usecase` classes for reusable business actions such as cache, delete, group update, startup maintenance, reading progress, and WebDAV flows.\n- Add a usecase when a new screen has meaningful business rules, when logic would otherwise be duplicated across features, or when a UI migration needs to extract meaningful business rules from an Activity/adapter.\n- Keep UI-only formatting and Compose layout decisions out of domain.\n- Keep entity-to-display mapping near the UI or ViewModel unless it is reused business language.\n\n## Compose Components and Theme\n\nPrefer project components before raw Material widgets:\n\n- Scaffold/top bars: `AppScaffold`, `GlassTopAppBar...`, `DynamicTopAppBar`, `TopBarActionButton`, `TopBarNavigationButton`.\n- Lists: `ListScaffold`, `TopFloatingStickyItem`, `VerticalFastScroller`, `SelectionBottomBar`.\n- Dialog/sheet: `AppAlertDialog`, `AppModalBottomSheet`, `OptionSheet`, text list input components.\n- Settings: `ClickableSettingItem`, `SwitchSettingItem`, `SliderSettingItem`, `ListSettingItem`, `SettingCard`.\n- Buttons/chips: `AppIconButton`, `SmallIconButton`, `SmallTextButton`, `ToggleChip`, `AlertButton`.\n- Covers/images: `CoilBookCover`, `BookshelfCover`, `buildCoverImageRequest`; use Coil for Compose image loading.\n- Theme: `LegadoTheme.colorScheme`, `LegadoTheme.typography`, `ThemeResolver`, `ProvideThemeOverride` where existing feature behavior needs theme override.\n\nAvoid introducing a second visual system. If a raw Material 3 component is used, style it from `LegadoTheme`.\n\n## Migration Checklist\n\nBefore editing:\n\n- Identify old XML layouts, ViewBinding fields, adapters, decorations, menu XML, dialogs, and result contracts.\n- Capture behavior: empty/loading/error states, refresh, search, sort, selection, long click, swipe, menu actions, back behavior, result codes, events, and persisted preferences.\n- Decide whether the task is UI-only or needs domain extraction.\n- For new screens, explicitly decide the presentation/domain/data boundary before coding; do not default to legacy mixed architecture.\n\nDuring editing:\n\n- Create/update `Contract`, then `ViewModel`, then `Screen`, then `MainActivity` route or compatibility host.\n- For new Compose destinations, wire navigation through `MainActivity` unless a legacy `Intent` caller must remain supported.\n- When retaining an Activity for compatibility, keep it small: parse legacy extras, collect effects, bridge result codes, and avoid placing feature business logic there.\n- Reuse existing string resources and add new resources for new user-facing text.\n- Register the ViewModel in `di/appModule.kt`.\n- Remove only resources made obsolete by this migration.\n- Keep existing public intent extra names and result codes unless explicitly changing an API.\n\nVerification:\n\n- Run `.\\gradlew.bat :app:compileAppDebugKotlin` for Kotlin-only changes.\n- Run `.\\gradlew.bat :app:assembleAppDebug` when resources, manifests, XML deletion, or generated bindings are affected.\n- If a screen uses Room queries or migration-sensitive repositories, run the relevant existing Android/unit tests if practical.\n- For manual verification, open the migrated screen and compare core flows against the old behavior list.\n\n## Common Pitfalls\n\n- Do not put navigation or Activity launchers inside composables.\n- Do not store one-time navigation as nullable fields in `UiState` unless the existing local pattern already does; prefer `SharedFlow` effects.\n- Do not keep adapter selection state split between old adapter and new Compose state.\n- Do not delete XML/menu resources until all references and generated binding imports are gone.\n- Do not add speculative domain layers for a single migrated screen.\n- Do not use mixed architecture for new screens unless compatibility with unreworked View UI requires it.\n- Do not make a migrated screen's retained Activity the primary navigation model when `MainActivity` can own the Compose destination.\n- Do not bypass `BaseComposeActivity` unless the screen must remain inside a Fragment or legacy host.\n- Do not replace existing event bus or service interactions as part of a UI migration unless required for correctness.\n","isInternal":false,"tokens":2794,"sizeBytes":13141},{"name":"SKILL.md","path":".codex/skills/legado-compose-migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.codex/skills/legado-compose-migration/SKILL.md","title":"legado-compose-migration","category":"anthropic-skill","format":"markdown","content":"---\nname: legado-compose-migration\ndescription: Guide Legado Android UI migration from XML/View/RecyclerView/DialogFragment screens to Jetpack Compose and Material 3, and guide new Compose-first screens using standard Android architecture. Use when Codex is asked to create, migrate, rewrite, review, or plan a Legado screen, Activity, Fragment, dialog, adapter, navigation destination, or settings page, especially when MainActivity navigation, MVI/UDF, StateFlow, Koin ViewModels, domain/usecase boundaries, or existing Compose component conventions matter.\n---\n\n# Legado Compose Migration\n\n## Overview\n\nMigrate one UI surface at a time, or create one new Compose destination at a time. Preserve behavior first for migrations; for newly created screens, prefer standard modern Android architecture over mixed legacy patterns. Use the project's existing `MainActivity` navigation, `BaseComposeActivity` compatibility hosts, `*Screen`, `*Contract`, `*ViewModel`, `StateFlow`, `SharedFlow`, Koin, theme, and widget patterns.\n\nBefore editing, inspect the target View implementation if one exists, `MainActivity` navigation when adding a destination, and at least two nearby migrated Compose screens. For concrete project patterns, including current Compose state/performance rules, read `references/project-patterns.md`.\n\n## Workflow\n\n1. State assumptions and success criteria.\n   - Name the exact screen or destination being created/migrated.\n   - For migrations, define behavior that must remain unchanged: inputs, result codes, navigation, menu actions, dialogs/sheets, list selection, refresh, persistence, and event bus behavior.\n   - For new screens, define the route owner, UI state, events/effects, domain/usecase boundary, and verification target.\n   - If the target mixes UI and business logic heavily, keep the migration surgical and defer deeper domain cleanup unless needed.\n\n2. Map the old surface.\n   - For migrations, read the Activity/Fragment, XML layouts, adapters, menu XML, dialogs, result launchers, and ViewModel.\n   - For new screens, read `MainActivity`, nearby route screens, the relevant ViewModel/usecase/repository patterns, and shared UI components.\n   - List UI state, user intents, one-shot effects, and external side effects.\n   - Identify reusable Compose components under `ui/widget/components` before creating new components.\n\n3. Choose the minimal migration shape.\n   - For new Compose-first screens, add a `MainActivity` navigation destination instead of creating a standalone Activity.\n   - Use `BaseComposeActivity` for full-screen Activity migrations only when a legacy Activity must remain as an entry point.\n   - Keep existing Activity Result APIs, `Intent` extras, permission flows, and Android framework calls in the host/compatibility Activity.\n   - If an unreworked View screen still starts the migrated screen with `Intent`, keep the old Activity only as a compatibility host that parses legacy extras and delegates to the Compose screen or `MainActivity` route boundary.\n   - Put renderable state in `UiState`, user actions in `Intent`, and one-shot navigation/framework work in `Effect`.\n   - For new screens, use standard Android/Compose architecture: UDF/MVI-style state hoisting, lifecycle-aware Flow collection, ViewModel-owned state, repository/usecase boundaries, and UI free of business logic.\n   - Use mixed legacy patterns only as an integration boundary for unreworked View screens or existing framework contracts.\n   - Use existing repositories/usecases when they already fit; introduce new domain/usecase classes when a new screen needs clean business boundaries or when a migration would otherwise duplicate or entangle business logic.\n\n4. Implement by layers.\n   - Create or update `FeatureContract.kt` first for `UiState`, `Intent`, `Effect`, dialog/sheet models, and menu action enums.\n   - Update `FeatureViewModel.kt` to expose `uiState: StateFlow<UiState>` and `effects: SharedFlow<Effect>`, with a single `onIntent(...)` entry point unless the existing feature has a simpler established pattern.\n   - Create `FeatureScreen.kt` as a stateless route-level composable: `state`, callbacks, and `onIntent`.\n   - Wire new Compose destinations through `MainActivity` route handling; update a retained Activity only when legacy `Intent` compatibility is required.\n   - Collect state with `collectAsStateWithLifecycle()` and collect effects in `LaunchedEffect(Unit)` from the route or compatibility host.\n   - Register new ViewModels in `di/appModule.kt` with `viewModelOf(::FeatureViewModel)` unless parameters require `viewModel { ... }`.\n\n5. Remove only obsolete migration artifacts.\n   - Delete XML layouts, adapters, menu resources, binding fields, and imports only when the migrated screen no longer references them.\n   - Do not refactor unrelated View screens or shared utilities.\n\n6. Verify.\n   - Prefer the smallest Gradle check that compiles the touched app code, usually `.\\gradlew.bat :app:compileAppDebugKotlin`.\n   - If resources, manifests, or XML deletion are involved, run `.\\gradlew.bat :app:assembleAppDebug` when feasible.\n   - For behavior-heavy changes, add or update focused tests only where the project already has a practical test seam.\n\n## Boundaries\n\n- Keep Compose functions side-effect-light. Use `LaunchedEffect` for collecting effects and use callbacks for user actions.\n- Do not pass `Activity`, `View`, binding objects, or mutable domain entities deep into composables unless an existing local pattern requires it.\n- Prefer project theme/components: `LegadoTheme`, `AppScaffold`, `ListScaffold`, `AppAlertDialog`, `AppModalBottomSheet`, `RoundDropdownMenu`, top bar helpers, setting items, cover components, and list utilities.\n- Prefer `StateFlow`/`SharedFlow` over `LiveData` for newly migrated Compose surfaces.\n- When using `FeatureIntent` for MVI user actions, distinguish it from Android `Intent` extras and launch APIs in names, comments, and explanations where both appear.\n- For new Compose-first screens, do not copy View-era shortcuts such as UI logic in Activity/Fragment, direct binding-like mutable UI state, adapter-owned state, or Activity-context business operations.\n- Keep one-off Android actions out of `UiState`: navigation, file opening, clipboard, dialogs implemented as Android DialogFragments, result launchers, permission requests, and callbacks that require host context should be `Effect`s handled by `MainActivity` route handling or a compatibility Activity.\n- Prefer `MainActivity` navigation for new Compose destinations. Treat standalone Activities for migrated screens as legacy entry points only when existing View code still depends on `Intent` navigation.\n- Keep existing Chinese string resources and localization behavior; add strings to resources when user-facing text is new.\n\n## Reference\n\nRead `references/project-patterns.md` when implementing or reviewing a migration. It contains project-specific examples, file placement rules, state/effect conventions, and verification commands.\n","frontmatter":{"name":"legado-compose-migration","description":"Guide Legado Android UI migration from XML/View/RecyclerView/DialogFragment screens to Jetpack Compose and Material 3, and guide new Compose-first screens using standard Android architecture. Use when Codex is asked to create, migrate, rewrite, review, or plan a Legado screen, Activity, Fragment, dialog, adapter, navigation destination, or settings page, especially when MainActivity navigation, MVI/UDF, StateFlow, Koin ViewModels, domain/usecase boundaries, or existing Compose component conventions matter."},"isInternal":false,"tokens":1432,"sizeBytes":7018},{"name":"openai.yaml","path":".codex/skills/legado-compose-review/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.codex/skills/legado-compose-review/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"interface:\n  display_name: \"Legado Compose Review\"\n  short_description: \"Review Legado Compose architecture quality\"\n  default_prompt: \"Use $legado-compose-review to review a Legado Compose screen for MVI, UDF, Clean Architecture, navigation, and project convention issues.\"\n","isInternal":false,"tokens":62,"sizeBytes":275},{"name":"review-checklist.md","path":".codex/skills/legado-compose-review/references/review-checklist.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.codex/skills/legado-compose-review/references/review-checklist.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Legado Compose Review Checklist\n\n## Files to Read\n\nFor a feature review, inspect the smallest complete slice:\n\n- `FeatureScreen.kt` and any `FeatureSheets.kt` / `FeatureDialogs.kt`.\n- `FeatureViewModel.kt`.\n- `FeatureContract.kt` if present.\n- Host route in `MainActivity.kt` or retained `FeatureActivity.kt`.\n- Koin registration in `di/appModule.kt`.\n- Repositories/usecases used by the feature.\n- Old View caller or XML/adapters only when still used for compatibility or behavior comparison.\n\nUse current examples as references:\n\n- `ui/main/MainActivity.kt` for Navigation3 route ownership.\n- `ui/book/info/BookInfoContract.kt`, `BookInfoViewModel.kt`, `BookInfoScreen.kt` for behavior-heavy MVI/UDF.\n- `ui/book/search/SearchContract.kt`, `SearchViewModel.kt`, `SearchScreen.kt` for route-level search state.\n- `ui/widget/components/...` and `ui/theme/...` for shared UI conventions.\n\n## Architecture Checks\n\nFlag issues when:\n\n- A new Compose-first screen is implemented as a standalone Activity instead of a `MainActivity` destination without a compatibility reason.\n- A retained Activity contains feature business logic instead of acting as a thin Android `Intent` compatibility host.\n- Business rules, repository calls, DAO calls, persistence writes, or service orchestration live in composables.\n- ViewModel exposes mutable state directly, exposes `MutableStateFlow`, or lets UI mutate domain objects.\n- UI state is split across Activity fields, composable `remember` state, adapter state, and ViewModel state in a way that can diverge.\n- One-shot events such as navigation, result codes, file opening, permission requests, or clipboard writes are represented as persistent `UiState` fields that can replay incorrectly.\n- MVI `FeatureIntent` user actions are confused with Android `Intent` launch/extras.\n- Domain/usecase boundaries are bypassed in new screens when a meaningful business action exists.\n- A migration introduces new domain abstractions for a single trivial UI action without reducing real complexity.\n\n## UDF and State Checks\n\nFlag issues when:\n\n- `Screen` functions own business state instead of receiving `state` and callbacks.\n- `remember` / `rememberSaveable` stores source-of-truth data that should survive process or route recreation through ViewModel state.\n- User-driven transient state that should survive recreation, such as a pending delete-confirmation target, is held with plain `remember` instead of `rememberSaveable` or ViewModel state.\n- `LaunchedEffect` keys are unstable or cause repeated data loading, duplicate navigation, duplicate toasts, or repeated service calls.\n- A long-lived `LaunchedEffect` collector captures parent callbacks or context-dependent values that can change without using `rememberUpdatedState`, unless the effect is intentionally keyed to restart.\n- Flows are collected without lifecycle awareness in UI routes where `collectAsStateWithLifecycle()` should be used.\n- List items lack stable keys where mutation, selection, or animation can make state attach to the wrong row.\n- Heterogeneous lazy lists or grids provide `key` but omit `contentType`, reducing composition reuse quality when headers, rows, ads, loading items, or expanded content mix in the same lazy layout.\n- Selection, search query, sorting, filtering, loading, and error states are not represented in a single coherent `UiState`.\n- Derived values are recomputed expensively on every recomposition instead of living in ViewModel state or `remember(key)`.\n- UI effects write list-derived consistency back into the ViewModel, such as pruning selection from `LaunchedEffect(uiState.items)`. Prefer deriving this in the ViewModel with `combine(...)` or reducing it when the data flow updates.\n- State is hoisted higher than the lowest common owner, such as a top-level route collecting a child ViewModel flow only needed inside a conditional branch or one popup menu.\n\n## Compose Stability and Collection Checks\n\nFlag issues when:\n\n- Collection-heavy `UiState` exposed to Compose uses ordinary Kotlin `List`, `Set`, or `Map` in hot paths. Kotlin 2.x strong skipping is helpful but does not make standard Kotlin collections stable to Compose.\n- DAO or repository lists are passed directly through `UiState` to deep composables without conversion to `ImmutableList` / `ImmutableSet` / `ImmutableMap` or a stable UI model wrapper.\n- Review recommendations imply replacing every internal collection. Keep the finding scoped to Compose-facing render state; temporary accumulators, sorting inputs, Room DAO signatures, and domain APIs can remain normal collections unless they are the actual recomposition boundary.\n- Mutable collections such as `ArrayList`, `MutableList`, or mutable maps are stored in Compose state or ViewModel `UiState`.\n- Entity classes from non-Compose modules are passed deeply through composables and cause visible recomposition churn; prefer stable UI render models if measurement or code shape shows this matters.\n\n## Navigation and Compatibility Checks\n\nFlag issues when:\n\n- New Compose destinations skip `MainActivity` Navigation3 route registration.\n- Legacy Android `Intent` extras or result codes change without an explicit compatibility plan.\n- A migrated screen is reachable both through `MainActivity` and a retained Activity with inconsistent state initialization.\n- Navigation is performed directly inside nested composables instead of through callbacks/effects.\n- Activity Result launchers, file pickers, permission requests, or Android DialogFragments are hidden inside reusable UI composables.\n- Back behavior bypasses ViewModel decisions when unsaved changes, selection mode, add-to-shelf prompts, or confirmation dialogs exist.\n\n## UI and Project Convention Checks\n\nFlag issues when:\n\n- Raw Material components ignore `LegadoTheme` or existing shared components where the project already has a wrapper.\n- A screen recreates shared UI already present under `ui/widget/components`.\n- User-facing text is hardcoded instead of using string resources, except for temporary debug-only text.\n- Compose UI keeps ViewBinding, adapter, or XML assumptions after migration.\n- Dialogs and bottom sheets use inconsistent project components when `AppAlertDialog`, `AppModalBottomSheet`, or existing option sheets fit.\n- Image loading bypasses existing Coil cover/image helpers where cover behavior, cache, SVG, or GIF handling matters.\n\n## Clean Architecture Checks\n\nFor new screens, expect:\n\n- Compose renders state and emits user actions.\n- ViewModel owns state, intent handling, and effect emission.\n- Usecases own reusable business actions.\n- Repositories mediate data access.\n- DAOs remain behind repositories unless adding a boundary would be disproportionate and local project patterns already allow direct use.\n\nFor migrated screens, allow pragmatic intermediate code only when:\n\n- It preserves behavior.\n- It is isolated behind a clear compatibility boundary.\n- It does not spread View-era assumptions into new Compose-first code.\n\n## Output Template\n\nUse this shape unless the user asks for another format:\n\n```text\nFindings\n- [P1] Title\n  file:line\n  Impact: ...\n  Fix: ...\n\nOpen Questions\n- ...\n\nNotes\n- No issues found in ... / Tests not run ...\n```\n\nWhen using Codex inline review comments, emit one directive per finding with tight line ranges:\n\n```text\n::code-comment{title=\"[P2] Keep route state in ViewModel\" body=\"...\" file=\"/absolute/path/FeatureScreen.kt\" start=42 end=45 priority=2 confidence=0.8}\n```\n\n## Verification Suggestions\n\nRecommend verification based on the reviewed change:\n\n- Kotlin-only review/fix: `.\\gradlew.bat :app:compileAppDebugKotlin`.\n- Resource/XML/manifest impact: `.\\gradlew.bat :app:assembleAppDebug`.\n- Navigation or compatibility changes: manually open both `MainActivity` route and any retained legacy Android `Intent` entry point.\n- Behavior-heavy ViewModel changes: add or run focused tests only where the project has a practical test seam.\n","isInternal":false,"tokens":1622,"sizeBytes":7946},{"name":"SKILL.md","path":".codex/skills/legado-compose-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/HapeLee/legado-with-MD3/HEAD/.codex/skills/legado-compose-review/SKILL.md","title":"legado-compose-review","category":"anthropic-skill","format":"markdown","content":"---\nname: legado-compose-review\ndescription: Review existing Legado Jetpack Compose code for architecture, behavior, maintainability, and project convention issues. Use when Codex is asked to audit, review, inspect, evaluate, or find problems in Legado Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, or early Compose implementations, especially for MVI/UDF, StateFlow/SharedFlow, Clean Architecture, MainActivity navigation, legacy Activity compatibility, and View-era mixed-pattern drift.\n---\n\n# Legado Compose Review\n\n## Overview\n\nReview existing Compose code before rewriting it. Focus on concrete defects, architectural drift, behavior risks, and missing verification, especially in early Compose screens that may predate the current MVI/UDF, Clean Architecture, and `MainActivity` navigation expectations.\n\nRead `references/review-checklist.md` for the project-specific checklist, current Compose state/performance checks, and severity guidance.\n\n## Workflow\n\n1. Define the review scope.\n   - Identify the exact screen, route, ViewModel, contract, or package under review.\n   - State whether the user wants review only or review plus fixes. If unclear, review first and do not edit code.\n   - Treat unrelated legacy View code as context, not as part of the review, unless it affects the Compose surface.\n\n2. Build context from code.\n   - Read the `*Screen`, `*ViewModel`, `*Contract`, host Activity/route, DI registration, and any repositories/usecases used by the feature.\n   - Read the old View implementation only if it is still a compatibility caller or behavior reference.\n   - Compare against nearby current examples such as `BookInfo*`, `Search*`, `MainActivity`, and shared components under `ui/widget/components`.\n\n3. Review by risk, not style preference.\n   - Prioritize behavior regressions, state duplication, lifecycle bugs, navigation bugs, business logic in UI, direct data access from UI/presentation, recomposition hazards, and missing compatibility handling.\n   - Flag style-only issues only when they conflict with established project conventions or make future migration harder.\n   - Do not require broad refactors for a small screen unless the current code creates real behavior or maintenance risk.\n\n4. Output findings first.\n   - Use code-review style: list findings before summaries.\n   - Include file and tight line references.\n   - Explain the concrete impact and the smallest credible fix.\n   - If using Codex review directives, emit one `::code-comment{...}` per finding.\n   - If no issues are found, say that clearly and mention remaining test/manual verification gaps.\n\n5. Suggest fixes only after findings.\n   - Group fixes into small, reviewable steps.\n   - For architecture drift, separate compatibility-preserving fixes from larger cleanup.\n   - Recommend `legado-compose-migration` only when the finding implies a migration or rewrite workflow.\n\n## Review Priorities\n\n- **P0/P1**: behavior breakage, lost navigation/result behavior, unsafe lifecycle collection, stale state, data corruption, crashes, or compatibility entry points that no longer work.\n- **P2**: architecture drift that will cause duplicated logic, hard-to-test behavior, recomposition bugs, or incorrect ownership of state/effects.\n- **P3**: convention drift, maintainability issues, weak naming, missing previews/tests, or small cleanup that should not block behavior.\n\n## Boundaries\n\n- Do not rewrite code during a review unless the user asks for fixes.\n- Do not demand pure architecture where a compatibility boundary is necessary for unreworked View screens.\n- For new Compose-first code, expect standard Android architecture: `MainActivity` route ownership, ViewModel-owned state, UDF/MVI user actions, `StateFlow`/`SharedFlow`, repositories/usecases, and UI without business logic.\n- For migrated code, allow a thin retained Activity only for Android `Intent` compatibility with unreworked View callers.\n- Distinguish MVI `FeatureIntent` user actions from Android `Intent` launch/extras when both appear.\n\n## Reference\n\nRead `references/review-checklist.md` for detailed checks. When a review turns into implementation, also read `../legado-compose-migration/references/project-patterns.md` if available.\n","frontmatter":{"name":"legado-compose-review","description":"Review existing Legado Jetpack Compose code for architecture, behavior, maintainability, and project convention issues. Use when Codex is asked to audit, review, inspect, evaluate, or find problems in Legado Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, or early Compose implementations, especially for MVI/UDF, StateFlow/SharedFlow, Clean Architecture, MainActivity navigation, legacy Activity compatibility, and View-era mixed-pattern drift."},"isInternal":false,"tokens":865,"sizeBytes":4248}],"systemPromptSnippet":"<yakaai_skills repo=\"HapeLee/legado-with-MD3\">\n<!-- File: AGENTS.md (Tokens: ~3948 | Category: root-instruction) -->\n# AGENTS.md\n\n本文件定义仓库级不变量。专项流程放在 `.agents/skills/`，长期方案放在 `docs/dev/`\n；不要把临时任务、讨论过程或一次性结论继续堆进本文件。\n\n## 资料优先级\n\n执行任务前按以下顺序建立上下文：\n\n1. 用户本次目标、范围与验收条件。\n2. 本文件的仓库级约束。\n3. 与任务匹配的 `.agents/skills/*/SKILL.md` 及其明确要求读取的 reference。\n4. `docs/dev/` 中的专项设计或迁移计划。\n5. 当前源码、测试、Gradle 配置和相邻实现；它们用于确认当前版本的真实 API 与行为。\n6. 外部资料只作为设计依据；涉及会变化的 Android、Kotlin、Compose 或库 API 时，以官方当前文档和本仓库版本为准。\n\n文档与源码不一致时，先指出差异。不要为了让实现符合过期文档而静默改代码。\n\n## Skill 路由\n\n- XML/View/Activity/Fragment/RecyclerView 到 Compose 的页面迁移、新 Compose 页面：使用\n  `legado-compose-migration`。\n- Compose Screen、Contract、ViewModel、导航、状态与兼容边界审查：使用 `legado-compose-review`。\n- Gradle 模块拆分、`commonMain` 抽取、KMP/CMP、`expect/actual`、平台能力适配、迁移门禁或相关脚手架：使用\n  `legado-kmp-migration`。\n- 单个任务可组合 skill，但只读取与当前工作切片有关的 reference。\n\n## 工作方式\n\n- 开始前说明假设、成功条件和不在范围内的内容。存在多种会显著改变结果的解释时，不静默选择。\n- 优先做最小、可回滚、可独立验证的垂直切片；不得借任务重构无关代码。\n- 修 bug 时先建立可复现测试或其他可观察证据；重构前后保持同一行为验证通过。\n- 保留工作区中用户已有修改。只移除本次改动产生的无用代码和资源。\n- 不为了“架构完整”创建空模块、单方法包装、无调用方抽象或通用 `Utils/Common` 容器。\n\n## 当前工程事实与目标方向\n\n当前工程是 Android-first 的渐进迁移仓库，不是已经完成的 KMP 工程：\n\n- Gradle 模块：`:app`、`:modules:book`、`:modules:rhino`、`:baselineprofile`；`modules/web` 是独立 Vue 3\n  工程。\n- `:app` 同时包含 Android UI、数据、领域、服务和大量遗留 View 代码；包目录只表达逻辑边界，尚无 Gradle\n  编译隔离。\n- 新 UI 以 Jetpack Compose、Material 3、Navigation 3、Koin、StateFlow/SharedFlow 为默认；阅读器渲染核心等成熟\n  View 代码可作为 Android 专业渲染岛保留。\n- `domain/` 是迁移目标边界，但当前仍存在 Android/JVM 类型渗透；不要把“目标纯净度”描述成已完成事实。\n- 长期目标是渐进演进为 KMP/CMP。详细目标结构、阶段和门禁见 `docs/dev/kmp-cmp-modernization.md`。\n\n### Feature-first 过渡结构\n\n- 新 Compose-first Feature 和完成整页迁移的代码使用 `io.legado.app.feature.<name>`；目录、Gradle\n  path、Kotlin package 分别采用 `feature/book-info`、`:feature:book-info`、`feature.bookinfo`。\n- 在 `:app` 内先形成稳定 Feature 包，再提升为真实 `:feature:<name>` 模块；只有确有多宿主/多模块消费者时才拆\n  `api/impl`，只有确有跨平台价值时才改为 KMP/CMP source sets。\n- Feature 内共置 Contract、ViewModel、Route、Screen 和私有 components/dialog/sheet/model；文件的详细归属与迁移步骤见\n  `docs/dev/feature-first-structure.md`，现有业务 owner 见 `docs/dev/feature-catalog.md`。\n- App host 聚合导航和 DI；Feature 不直接依赖其他 Feature 实现。领域、数据、平台能力不能为了目录整齐塞入\n  Feature UI 包。\n- 旧 `ui/...` 是渐进迁移区，不做全量机械搬家；一个 Feature 的新 owner 建立后，禁止继续向旧包新增同职责文件。\n\n### 目标依赖方向\n\n```text\napp host / composition root\n        ↓\nfeature implementation → feature API\n        ↓\ndomain / model / platform contracts\n        ↓\ndata abstractions\n        ↓\nplatform implementations (Android/JVM/iOS/...)\n```\n\n- 应用宿主负责组装 Feature 实现、DI、平台生命周期和导航运行时。\n- Feature 之间通过稳定契约或导航 API 协作，不直接依赖其他 Feature 的实现。\n- `commonMain` 只容纳经过依赖审计、能被至少一个非 Android 目标编译验证的代码；移动目录不等于完成跨平台迁移。\n- Android `Context`、Room 实体/DAO、Service、Broadcast、Activity Result、Cronet、文件\n  URI、通知、媒体会话、成熟阅读器渲染和现有 Rhino 集成默认留在平台侧，通过窄接口进入共享层。\n- 平台能力不可用时必须显式建模为 capability/unsupported，不得用静默空实现伪造跨平台支持。\n\n## Compose 屏幕约束\n\n- 新屏幕使用 Compose；不要新增 XML/View Activity/Fragment。\n- Android Compose 新屏幕采用 UDF/MVI：`@Stable XxxUiState`、`XxxIntent`、`XxxEffect`；ViewModel 直接继承\n  `ViewModel`，私有 `MutableStateFlow` / `MutableSharedFlow(extraBufferCapacity = 16)`，对外只暴露只读\n  Flow，并由单一 `onIntent` 分发用户动作。共享 Feature 的状态宿主按 KMP skill 单独决策。\n- Screen/Content 保持无业务逻辑、无 DAO/网络/存储直连；ViewModel 通过 Gateway/Repository/UseCase\n  访问业务能力。\n- 所有 UiState 与 UI item 数据类标注 `@Stable`。Compose 渲染边界中的集合使用\n  `kotlinx.collections.immutable`；内部计算和数据层不机械替换集合类型。\n- 导航、权限、文件选择和其他宿主动作通过回调或 Effect 处理。新目的地优先由 `MainActivity` 的 Navigation\n  3 图组装。\n- 使用项目的 `AppScaffold`、主题、top bar、dialog/sheet、列表和设置组件。正确处理 edge-to-edge 与\n  predictive back。\n- Activity 仅在遗留 `Intent`/result 兼容确有需要时作为薄宿主保留。\n\n具体文件形态和审查清单由 Compose migration/review skill 维护，本文件不复制完整模板。\n\n## 数据与设置边界\n\n- UI（包括 ViewModel）不得新增 DAO、`appDb.*Dao`、网络客户端或旧偏好 API 直连；历史债务由\n  `verifyConfigArchitecture` 的基线棘轮冻结并逐步下调。\n- 普通设置 Gateway 通过 `update { current -> current.copy(...) }` 修改状态；关联字段在一次\n  `copy(...)` 中原子提交。\n- 不引入 `*SettingsUpdate` 分发类型或 `updateAll`。\n- `ReadStyleMutation`、`ThemePackageSettingsGateway.applyAndAwait`、`ThemeStateTransaction`、\n  `AppUiConfigurationGateway` 等专用 API 保持现有形态。\n- Koin 中 Gateway 接口到 Repository 实现保持显式绑定；不要用构造函数绑定掩盖接口归属。普通\n  Repository/UseCase/ViewModel 可继续使用项目既有 `singleOf` / `viewModelOf` 约定。\n- 新的共享领域契约不得暴露 `Context`、`File`、`Uri`、Room 类型、Android resource id 或 JVM-only\n  stream；用领域值、`ByteArray`/抽象 source-sink、序列化模型或平台接口表达。\n\n## KMP/CMP 迁移纪律\n\n- 先写行为/契约测试和依赖清单，再移动代码；一个 PR 只完成一个可说明的边界变化。\n- 优先抽取稳定模型、纯函数、规则解析契约和 UseCase；网络、数据库、JS 引擎、服务、阅读器渲染最后通过平台适配器处理。\n- `expect/actual` 只用于真正的平台原语。可用普通接口 + DI 表达的能力，不使用 `expect/actual`。\n- 平台实现由 Koin 和应用 composition root 组装。\n- 不因 KMP 目标同时替换数据库、网络、DI、导航和 UI 框架。每次只改变一个主要风险维度。\n- Feature `api/impl` 只有形成真实 Gradle 边界并存在跨 Feature/宿主调用时才创建，不能只建同名文件夹。\n- 首个非 Android 目标是架构证明，不承诺功能齐平；目标顺序由 capability matrix 和产品需求决定。\n\n## 代码生成\n\n- 生成器用于编码已稳定的约定，不用于发明架构。至少有两个手工迁移且通过审查的同类样本后，才能固化模板。\n- 脚手架必须可预览或 dry-run、拒绝覆盖已有文件、输出最小文件集，并在已有 Feature 上采用追加/AST\n  感知修改，禁止覆盖 route graph 或 DI 文件。\n- 生成的可提交源码仍接受普通代码审查；真正的编译期生成物放入 `build/generated`，不得手工编辑或提交。\n- 模板和生成器变更必须有 fixture/snapshot 或编译验证，并与 convention plugin 的模块类型保持一致。\n\n## 重构审查门禁\n\n每个迁移 PR 至少回答：\n\n- 行为基线是什么，哪些测试或手工路径证明没有回退？\n- 依赖边界净变化是什么，是否减少了 Android/JVM 泄漏或历史基线？\n- 新模块/接口是否有真实调用方，能否用更小改动完成？\n- Android 实现与共享契约的错误、线程、取消、序列化语义是否一致？\n- 做了哪些验证，哪些真机、性能、外部服务或其他平台行为尚未验证？\n- CI 是否执行了受影响的共享测试、目标编译或 adapter contract test？\n\n减少历史违规时必须同步下调对应 baseline；门禁不接受“先放宽基线再迁移”。详细分级见 KMP/CMP 现代化计划。\n\n## 构建与验证\n\n开发与 CI 使用 JDK 21。按风险选择最小充分验证：\n\n```powershell\n# Kotlin/Compose 快速编译\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# 当前主验证集\n.\\gradlew.bat testAppDebugUnitTest lintAppDebug verifyConfigArchitecture assembleAppDebug --continue --no-configuration-cache\n\n# 资源、Manifest、生成绑定或打包变化\n.\\gradlew.bat :app:assembleAppDebug\n\n# Release/R8 相关\n.\\gradlew.bat assembleAppRelease\n.\\gradlew.bat assembleAppNoR8\n\n# Web 前端\npnpm --dir modules/web build\n```\n\nKMP 任务名在模块实际创建后才存在；不要假装运行尚未定义的 `commonTest`、目标编译或 API\n检查任务。新增模块时把其真实任务加入 CI，并在交付中列出准确命令。\n\n所有文本改动至少运行 `git diff --check`。构建通过不替代架构边界、行为和真机性能复核。\n\n## 重要项目约束\n\n- 不将 jsoup 升级到 1.16.2 以上；新版行为会影响 `AnalyzeByJSoup.kt` 与 JsoupXpath。\n- 应用代码仅在两处依赖 Hutool：①作为书源 JS 的运行时加密库保留在 classpath（书源直接调用\n  `Packages.cn.hutool.*`）；②`help/crypto/CryptoUtils.kt` 的 base64 **解码**统一走\n  `cn.hutool.core.codec.Base64.decode`，以兼容缺 `=` 填充等宽松输入（Kotlin\n  `kotlin.io.encoding.Base64` 严格填充会导致部分书源取章名/正文回归）。版本固定 5.8.22 勿升级。\n  应用内部加密主体仍用现有 JCA（`javax.crypto`/`java.security`）与 `help/crypto` 路径，KMP 抽取时\n  再通过能力契约替换 JVM API。\n- 代码 namespace 为 `io.legado.app`，Android `applicationId` 为 `io.legato.kazusa`，不要混用。\n- 当前 minSdk 26、target/compile SDK 37；Release 启用 R8 与资源压缩，`noR8` 变体用于排障。\n- Rhino 书源/RSS/TTS 规则、Android 服务、Web 服务和阅读器渲染属于高行为风险平台能力，迁移前必须建立兼容测试或\n  capability 边界。\n- `modules/web` 必须连接应用内 Ktor WebService；开发环境通过 `VITE_API` 指向设备服务地址。\n\n## 交付说明\n\n最终说明应包含：修改的职责范围、关键设计取舍、实际运行的验证与结果、未验证风险。不要把“已生成代码”或“已移动文件”当作完成标准。\n\n<!-- File: CLAUDE.md (Tokens: ~3763 | Category: claude-rule) -->\n# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis is a Material Design 3 fork of [Legado](https://github.com/gedoor/legado). `app/src/main/java/io/legado/app/` uses **Clean Architecture** with three layers:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `:app`, `:modules:book` (epub/TXT parsing, namespace `me.ag2s`), `:modules:rhino` (Rhino JS wrapper, namespace `com.script`). There is also a Vue 3 web frontend in `modules/web/` (pnpm, separate from the Android build).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`MainActivity` holds a single `NavDisplay` with `entryProvider { ... }` defining all composable entries. `Launcher0` through `LauncherW` extend `MainActivity` to provide multiple launcher icon alias entries. Separate activities handle the reader (`ReadBookActivity` — still View-based), book info, source management, replace rules, file manager, QR scanner, etc.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 theme modes (`AppThemeMode` enum) — Dynamic (Monet), 12 named presets, Custom (MaterialKolor seed-color generation), Transparent. `CustomColorScheme` wraps `com.materialkolor` with configurable `PaletteStyle` (TonalSpot, Neutral, Vibrant, Expressive, Rainbow, etc.) and `ColorSpec` (2021 vs 2025).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.claude/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook sources, RSS sources, and HTTP TTS use JavaScript rules. `initRhino()` in `App.kt` registers `NativeBaseSource` wrappers for `BookSource`, `RssSource`, `HttpTTS` (writable JS objects) and `ReadOnlyJavaObject` wrappers for rule entities. Rule parsing logic lives in `help/source/` and `model/analyzeRule/`.\n\n## Important Constraints\n\n- **Do not update jsoup** beyond 1.16.2 — a breaking change in newer versions (see [jsoup#2017](https://github.com/jhy/jsoup/pull/2017)) affects `AnalyzeByJSoup.kt` and the JsoupXpath library\n- Hutool is back on the classpath at 5.8.22 (do not upgrade). Book-source JS calls it via\n  `Packages.cn.hutool.*`; app-internal **base64 decoding** in `help/crypto/CryptoUtils.kt` also\n  routes through `cn.hutool.core.codec.Base64.decode` for lenient input compatibility (Kotlin\n  `kotlin.io.encoding.Base64` is strict about `=` padding). App crypto otherwise uses JCA (\n  `javax.crypto`/`java.security`); new internal tools live in `help/crypto/CryptoUtils.kt`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated in `modules/web/` — a Vue 3 + TypeScript + Vite project for remote bookshelf and source editing. Must connect to the app's built-in HTTP server (started via `WebService` in the main activity settings). Commands:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~265 | Category: copilot-instructions) -->\n# Copilot repository instructions\n\nRead and follow `/AGENTS.md` before changing this repository. It is the source of truth for\nproject-wide constraints, source priority, validation, current Android boundaries, and the KMP/CMP\ndirection.\n\nFor specialized work, also read the matching repository skill:\n\n- `/.agents/skills/legado-compose-migration/SKILL.md` for View/XML to Compose or new Android Compose\n  screens.\n- `/.agents/skills/legado-compose-review/SKILL.md` for Compose architecture and behavior review.\n- `/.agents/skills/legado-kmp-migration/SKILL.md` for modularization, `commonMain`, CMP, platform\n  adapters, migration gates, and scaffolding.\n\nDo not infer current package names, Gradle tasks, dependencies, navigation APIs, or platform support\nfrom examples. Confirm them in source, tests, `settings.gradle`, Gradle build files, and\n`gradle/libs.versions.toml`. The KMP target structure in `docs/dev/kmp-cmp-modernization.md` is a\nstaged proposal, not a description of modules that already exist.\n\nKeep changes surgical, preserve existing workspace edits, run risk-matched verification, and report\nexact commands plus unverified behavior.\n\n<!-- File: .agents/skills/legado-compose-migration/references/project-patterns.md (Tokens: ~1676 | Category: anthropic-skill) -->\n# Legado Android Compose Project Patterns\n\nRead this reference while implementing a screen. Confirm every referenced API against the current\ncheckout because the repository is mid-migration.\n\n## Ownership and placement\n\n- Android application module: `:app`.\n- Canonical new Feature package: `io.legado.app.feature.<name>`.\n- `ui/...` is legacy/migration territory. Keep only explicit compatibility owners there after a\n  Feature establishes its canonical package.\n- `MainActivity` owns new in-app Navigation 3 destinations and the root graph.\n- A retained Activity translates stable external/legacy Intent inputs and results; it is not a\n  second presentation or navigation owner.\n- App-level Koin aggregation remains in the host. Use the repository's existing `viewModelOf` or\n  parameterized `viewModel` convention rather than adding a second DI pattern.\n\nA Feature may contain Contract, ViewModel, Route, Screen, components, dialogs, sheets and\npresentation models, but create only the files its behavior needs. Directory shape is not an\nacceptance criterion.\n\n## State and effects\n\nFor a behavior-heavy new screen, the repository expects:\n\n- `@Stable` Compose-facing `UiState` and UI item models;\n- immutable collections at the Compose rendering boundary;\n- private `MutableStateFlow`, exposed as read-only `StateFlow`;\n- when best-effort transient effects are actually needed, private\n  `MutableSharedFlow(extraBufferCapacity = 16)`, exposed as read-only `SharedFlow`;\n- one `onIntent` dispatcher for user actions;\n- host actions expressed as effects or callbacks.\n\n`@Stable` is an assertion, not a magic optimization. Every public property must remain stable and\nchanges observed by Compose. Wrap unstable data only when the wrapper has correct equality and\nmutation semantics. Use measurement/compiler reports before inventing performance abstractions.\n\nKeep source-of-truth business state in the ViewModel. Local `remember`/`rememberSaveable` is\nsuitable\nfor UI affordances and restorable drafts/IDs, not a second copy of repository state. Avoid\nUI-to-ViewModel feedback loops; restore consistency in the reducer or flow that produces the state.\n\nClassify host actions by delivery semantics:\n\n- A user click whose only meaning is UI navigation can call a host callback directly; use a\n  lifecycle-aware rapid-click guard such as `dropUnlessResumed` where the available Lifecycle\n  version supports it.\n- A ViewModel outcome that must survive a missing collector becomes state with an acknowledgement\n  or another explicit durable protocol.\n- Snackbar/toast/haptic feedback that is intentionally best-effort may use the Feature effect\n  stream. Document the loss behavior rather than assuming `extraBufferCapacity` solves it.\n\n## Host boundaries\n\nKeep these at the route/host unless the project already has a tested abstraction:\n\n- navigation and result delivery;\n- permission and Activity Result launchers;\n- file/document pickers;\n- Android framework dialogs and services;\n- Context-dependent clipboard, URI and external-app operations.\n\nReusable Screen functions receive state and semantic callbacks. They do not know an Activity,\nbinding, DAO, application singleton or root back stack.\n\n## Lists, lifecycle and recomposition\n\n- Use stable keys when item identity survives insertion, removal or reorder; use `contentType` when\n  heterogeneous reuse matters.\n- Collect Android route state with `collectAsStateWithLifecycle` unless another lifecycle is\n  deliberate and documented. Apply the same delivery analysis to effect collectors; composition\n  lifetime and host `RESUMED` state are not interchangeable.\n- Key `LaunchedEffect` by the lifetime it represents. Use `rememberUpdatedState` for changing\n  callbacks captured by an effect that should not restart.\n- Derive cheap display values in composition; memoize or move work only when its cost/lifetime\n  warrants it.\n- Prefer immutable Compose-facing collections, while leaving temporary computations and data-layer\n  APIs in their natural collection types.\n- Strong Skipping is enabled by default on modern Kotlin/Compose compiler versions. Unstable\n  parameters are compared by identity, so avoid needless instance churn, but do not add wrappers or\n  `@Stable` solely from intuition. The repository requires the annotation on UI state/item types;\n  those types must actually satisfy its equality and observable-mutation contract.\n\n## Insets and back\n\nTrace which layer owns each inset. For a Material 3 `Scaffold`, inspect its configured\n`contentWindowInsets` and verify that content applies/consumes the provided padding correctly.\nSheets, dialogs, IME and nested scaffolds may need separate treatment. Visual/manual evidence is\nrequired; the presence of `Scaffold`, `safeDrawing` or padding modifiers alone proves nothing.\n\nNavigation 3 integration does not remove the need to verify custom back interception, selection\nmode, unsaved changes and retained Activity entry points. Route back actions through the same owner\nthat decides whether leaving is allowed. Use `PredictiveBackHandler` when gesture progress drives\nUI;\nwhen customizing `NavDisplay` transitions, also supply/test predictive-pop behavior.\n\n## Adaptive layouts and accessibility\n\nBecause this app targets API 37, large-screen orientation, aspect-ratio and resizability\nrestrictions\ncannot be used as a compatibility fallback. Treat the current app window as dynamic across rotation,\nfold/unfold, split-screen and desktop windowing.\n\n- Test compact and expanded widths for new or substantially migrated destinations.\n- Use current window metrics/window size classes for layout decisions; do not branch on a physical\n  device category or assume portrait.\n- Preserve important input/draft/selection state across recreation with the appropriate local\n  saveable state or `SavedStateHandle`, based on the state owner and size.\n- Do not stretch a phone layout indefinitely. Adopt list-detail/supporting panes or adaptive\n  navigation only when the Feature benefits; avoid adding a library merely to satisfy a checklist.\n- Prefer standard interactive components/modifiers. Custom pointer input needs semantic actions,\n  focus/keyboard access and a usable touch target.\n\n## Architecture boundary\n\nNew UI and ViewModels do not add DAO, `appDb`, network-client or old preference access. Use existing\nGateway/Repository/UseCase contracts, or add the smallest real boundary with an actual caller.\n\nFor a strictly UI-only migration, an existing presentation-layer violation may remain to avoid\ncombining architecture and UI rewrites. Freeze rather than duplicate it, document it, and do not\ndescribe the screen as fully modernized until the boundary is corrected.\n\n## Verification selection\n\n- Kotlin-only: `:app:compileAppDebugKotlin`.\n- Resources/manifest/XML/generated binding/package: `:app:assembleAppDebug`.\n- Changed state transitions: focused unit tests with success/failure/cancellation cases as relevant.\n- Navigation or compatibility: exercise the MainActivity route and every retained Intent/result\n  entry.\n- Insets, IME, accessibility and predictive back: device/emulator evidence where risk warrants it.\n- New/substantially changed destinations: representative compact and expanded window checks plus\n  rotation/recreation; add targeted adaptive UI tests when layout branching is meaningful.\n\nUse `AGENTS.md` for the canonical full verification set and exact wrapper command.\n\n## Current official references\n\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting)\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle)\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events)\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n- [Compose side effects](https://developer.android.com/develop/ui/compose/side-effects)\n- [Material Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n- [Adaptive layouts for resizable apps](https://developer.android.com/develop/adaptive-apps/guides/app-orientation-aspect-ratio-resizability)\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n\n<!-- File: .agents/skills/legado-compose-migration/SKILL.md (Tokens: ~1418 | Category: anthropic-skill) -->\n---\nname: legado-compose-migration\ndescription: Create or migrate a Legado Android screen with Jetpack Compose while preserving behavior and following the repository's Feature ownership, UDF, navigation, DI, inset, and compatibility boundaries. Use for XML/View/RecyclerView/DialogFragment migrations and new Android Compose destinations. Use legado-compose-review instead for review-only requests, and legado-kmp-migration when the change crosses a multiplatform or Gradle boundary.\n---\n\n# Legado Compose Migration\n\n## Purpose\n\nDeliver one working Android UI surface without importing View-era ownership into new Compose code.\nFor migrations, preserve observable behavior before cleanup. For new screens, use the repository's\ncurrent Feature-first and UDF conventions.\n\nBefore editing, read `AGENTS.md`, the relevant part of `docs/dev/feature-first-structure.md`, the\ncurrent implementation and one or two nearby examples that exercise the same behavior. Read\n[references/project-patterns.md](references/project-patterns.md) for repository-specific placement,\nstate and host details; examples are evidence, not templates to copy blindly.\n\n## Decide the shape\n\n- **New destination:** prefer a `MainActivity` Navigation 3 destination. Do not create a standalone\n  Activity without an external-entry or platform lifecycle reason.\n- **Migrated destination:** retain an Activity only as a thin compatibility host when existing\n  callers require stable Android Intent extras/results or a framework contract.\n- **Partial migration:** keep mature View rendering or platform integrations as explicit islands\n  when replacing them would add behavioral risk unrelated to the request.\n- **Multiplatform boundary:** stop treating this as only an Android screen migration and also use\n  `legado-kmp-migration`.\n\n## Workflow\n\n1. Bound the surface and success criteria. Record inputs, navigation/results, empty/loading/error\n   states, actions, dialogs/sheets, persistence, back behavior and external effects that must\n   remain.\n2. Read the smallest complete slice: UI, state owner, host/route, DI, resources, data/use-case\n   calls,\n   and any old implementation needed as a behavior reference.\n3. Choose the lowest state owner that reads and writes each value. Put durable business/render state\n   in `UiState`; keep purely local focus, expansion, animation and draft state in the UI when no\n   higher owner needs it. Route Feature/business actions through the Feature intent type. A direct\n   user action that only requests host navigation may use a host callback. Dialog/sheet visibility\n   is UI state; Android framework dialogs, permissions, file pickers and launches stay in the host.\n4. Keep Screen/Content free of repository, DAO, network, storage and service access. New\n   presentation\n   code uses Repository/Gateway/UseCase boundaries. Preserve an existing violation only when the\n   task is explicitly UI-only, do not spread it, and record the remaining debt.\n5. Wire navigation, DI and compatibility behavior at the host boundary. Keep public extras, result\n   codes and deep-link semantics stable unless the user requested an API change.\n6. Delete XML, adapters, bindings and resources only after references and compatibility callers are\n   gone.\n7. Verify in proportion to risk and report both passed evidence and unverified manual/device paths.\n\n## Project constraints\n\n- New ownership lives under `io.legado.app.feature.<name>`; legacy `ui/...` is a migration source,\n  not the destination for a second implementation.\n- Use ViewModel-owned read-only `StateFlow` and one `onIntent` dispatcher for Feature/business\n  actions, as required by `AGENTS.md`. When a Feature has best-effort transient effects, expose the\n  project's `SharedFlow(extraBufferCapacity = 16)` shape. Buffer capacity does not make emissions\n  durable while no UI is collecting: navigation/result/destructive completion and other\n  consistency-critical ViewModel outcomes must reduce to state or use an explicit acknowledgement\n  protocol instead of relying on an effect stream.\n- Annotate Compose-facing `UiState` and UI item models with `@Stable` only while honoring the\n  annotation's contract: public mutable properties and unobservable mutation make it incorrect.\n  Use immutable collections at the rendering boundary; do not change repository/data collections\n  mechanically.\n- On Android, collect `Flow` UI state with `collectAsStateWithLifecycle` unless a documented\n  lifecycle requirement calls for something else. A `LaunchedEffect` collector is scoped to the\n  composition, not automatically to `STARTED`/`RESUMED`. Long-lived effects must use appropriate\n  keys and `rememberUpdatedState` for changing values that should not restart them.\n- Reuse project theme and components when their behavior fits. A wrapper's existence is not enough\n  reason to use it when semantics, accessibility or host requirements differ.\n- Insets are an ownership decision. Inspect `Scaffold` `contentWindowInsets`, whether content\n  consumes `innerPadding`, nested scaffolds/sheets and IME behavior. Do not assume that using\n  `Scaffold` alone proves edge-to-edge correctness, and do not double-apply system-bar padding.\n- Preserve predictive-back behavior. Use ordinary `BackHandler` only for binary interception; use\n  `PredictiveBackHandler` when custom UI needs gesture progress. If Navigation 3 transitions are\n  customized, define and verify predictive-pop behavior as well as the normal pop transition.\n- The app targets API 37, where orientation, aspect-ratio and resizability restrictions no longer\n  protect layouts on large screens. New or substantially migrated destinations must tolerate\n  compact, medium and expanded resizable windows, rotation and recreation. Base layout decisions on\n  the current app window, not physical-device assumptions; do not add an adaptive library or\n  multi-pane layout unless the screen's behavior benefits from it.\n- Prefer Material/Foundation interactions for their semantics, focus and keyboard behavior. Custom\n  controls and gestures must provide appropriate role/state/action semantics, keyboard/D-pad access\n  and an adequate touch target.\n- Keep localized user-facing text in resources and retain fallback behavior.\n\n## Verification\n\n- Kotlin-only presentation/host changes: run `:app:compileAppDebugKotlin` using the repository's\n  Gradle wrapper syntax for the current shell.\n- Resource, manifest, XML/binding deletion or packaging changes: run `:app:assembleAppDebug`.\n- State/business changes: run focused ViewModel/use-case tests or add a characterization seam.\n- Navigation/Intent/results/insets/back: manually or instrumentedly exercise every retained entry\n  path that compilation cannot prove.\n- New or substantially changed layouts: exercise representative compact and expanded resizable\n  windows, rotation/recreation and IME interaction rather than validating only one portrait phone.\n- Always run `git diff --check`.\n\nUse actual task names from this checkout; do not invent tasks from this skill.\n\n<!-- File: .agents/skills/legado-compose-review/references/review-checklist.md (Tokens: ~976 | Category: anthropic-skill) -->\n# Legado Compose Review Checklist\n\nUse only the sections relevant to the scoped surface.\n\n## Context\n\n- Confirm the canonical Feature owner and whether any legacy `ui/...` file is a compatibility owner.\n- Read Screen/Content, ViewModel/state contract, route/Activity, DI and directly used domain/data\n  contracts.\n- Read old XML/View/adapters only when needed for parity or a live caller.\n- Identify all entry points: MainActivity route, Intent/deep link, notification/widget/service or\n  Activity Result.\n\n## Behavior and state\n\n- Is there one durable state owner, or can ViewModel, Activity, `remember`, adapter and repository\n  copies diverge?\n- Is each state hoisted only to the lowest owner that needs to read and write it? Local expansion,\n  focus or animation state need not be forced into a ViewModel; business state must not be copied\n  into local Compose state.\n- Can loading, empty, error, selection, query, sorting and dialog/sheet states be reconstructed\n  after\n  recreation where required?\n- Are one-shot effects consumed once without becoming persistent replayable flags?\n- Can a ViewModel-produced effect be emitted while the route has no active collector? If losing it\n  would leave navigation, results or durable business state inconsistent, model/acknowledge it as\n  state rather than relying on `SharedFlow` buffering.\n- Are success, failure, cancellation and stale-response ordering handled?\n- Do `LaunchedEffect` keys match the intended lifetime and avoid duplicate loads/navigation/toasts?\n- Is `Flow`-backed Android UI state collected with `collectAsStateWithLifecycle`, or is there a\n  documented lifecycle reason for another collector?\n- Are mutable collections/entities exposed in a way that invalidates a claimed `@Stable` contract?\n- With Strong Skipping, are unstable parameters recreated on each update and compared by identity?\n  Ask for compiler/measurement evidence before treating ordinary recomposition as a defect.\n- Do lazy items use stable identity where insert/remove/reorder can attach state to the wrong row?\n\n## Architecture and ownership\n\n- Screen/Content renders state and emits semantic actions; it does not access DAO, repository,\n  network, storage, service or global application state.\n- New ViewModel code uses Gateway/Repository/UseCase boundaries and does not add DAO or old global\n  access.\n- Host owns navigation, framework launchers, permissions, Context/URI work and root lifecycle.\n- A retained Activity translates compatibility inputs/results without becoming a second business or\n  navigation owner.\n- Feature code does not depend on another Feature implementation.\n- New files do not extend a legacy package after a canonical Feature owner exists.\n\n## Navigation and compatibility\n\n- New in-app destinations are registered in the current MainActivity graph unless an external-entry\n  requirement justifies an Activity.\n- Existing extras, result codes, deep links and callers preserve semantics.\n- Multiple entry paths initialize equivalent state and converge on the same owner.\n- Nested UI requests navigation through callbacks/effects rather than mutating the root stack.\n- Pure UI navigation can call a host callback directly. Check rapid taps and lifecycle state; use a\n  lifecycle-aware guard such as `dropUnlessResumed` where supported rather than inventing a\n  ViewModel effect solely to move the navigation call.\n- Back interception covers real selection/unsaved state without breaking predictive back or host\n  result delivery.\n- Custom gesture-progress UI uses `PredictiveBackHandler`; customized Navigation 3 pop transitions\n  also define/verify predictive-pop behavior.\n\n## UI, Insets and accessibility\n\n- Trace `Scaffold` `contentWindowInsets`, inner padding consumption, nested scaffolds/sheets, system\n  bars and IME. Check for both overlap and double padding.\n- User-facing strings retain localization and fallback behavior.\n- Project components/theme are used when their semantics fit; wrappers are not applied mechanically.\n- Touch targets, focus order, semantics/content descriptions and keyboard behavior fit the control.\n- Custom pointer input has an accessible semantic action and keyboard/D-pad alternative; standard\n  Material/Foundation interactions are preferred when their behavior fits.\n- Lists/images preserve existing cache, loading and accessibility behavior where relevant.\n- Removed XML/binding/menu resources have no remaining references.\n\n## Verification and output\n\nTie each finding to evidence and the smallest fix. Recommend only checks that can observe the risk:\n\n- compilation for Kotlin wiring;\n- assemble for resources, manifest, XML/binding and packaging;\n- focused unit tests for reducers/ViewModels/use cases;\n- route plus retained Intent/result path checks for navigation compatibility;\n- device/emulator checks for Insets, IME, accessibility, predictive back and rendering performance.\n\nUse P0/P1/P2/P3 from the skill. If there are no findings, explicitly list what was not executed or\nobserved.\n\n<!-- File: .agents/skills/legado-compose-review/SKILL.md (Tokens: ~1283 | Category: anthropic-skill) -->\n---\nname: legado-compose-review\ndescription: Review existing Legado Android Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, and compatibility hosts for concrete behavior, lifecycle, state ownership, architecture, inset, accessibility, and maintainability risks. Use for review or audit requests. Do not rewrite code unless fixes are requested; use legado-compose-migration for implementation and legado-kmp-migration for multiplatform or Gradle-boundary findings.\n---\n\n# Legado Compose Review\n\n## Purpose\n\nFind concrete defects and risky drift before proposing rewrites. Judge the code against current\nrepository behavior and constraints, not a preferred template.\n\nRead `AGENTS.md`, the scoped files and\n[references/review-checklist.md](references/review-checklist.md). Consult an old View implementation\nonly when it remains a caller or behavior baseline.\n\n## Review workflow\n\n1. Define the exact surface and whether the user asked for review only or fixes. Review-only work\n   does not authorize edits.\n2. Inspect the smallest complete behavior slice: Screen, state owner/contract, route/host, DI,\n   relevant repositories/use cases, resources and compatibility entry points.\n3. Trace state and effects end to end. Look for divergent owners, replayed one-shot work, lifecycle\n   mistakes, lost events, unstable identity and error/cancellation paths before style issues.\n4. Trace navigation/results, Insets/IME, back, accessibility and platform launchers through the real\n   host. Do not infer correctness from the presence of a particular API.\n5. Report findings first with tight line references, concrete impact and the smallest credible fix.\n   Separate compatibility-preserving fixes from optional redesign.\n6. If no finding exists, say so and list the important unverified runtime/device paths.\n\n## Severity\n\n- **P0/P1:** crash, data/behavior loss, broken navigation/result/entry compatibility, unsafe\n  lifecycle or concurrency, repeated destructive effect, inaccessible/obscured critical content.\n- **P2:** state ownership that can diverge, business/data access in UI, platform leakage, incorrect\n  stability contract, recurring recomposition/performance issue, or architecture drift with a real\n  maintenance cost.\n- **P3:** localized convention, naming, testability or cleanup issue without demonstrated behavior\n  or structural impact.\n\nDo not promote a missing annotation, different file split or wrapper choice to P2 without explaining\nthe observable or architectural consequence.\n\n## Boundaries\n\n- Do not require every screen to have the same number of files or abstractions.\n- Allow thin compatibility Activities and mature View/platform islands when they have a current\n  caller and one clear owner.\n- New Compose presentation must not add DAO, network, storage or service access; report existing\n  debt without turning a review into an unauthorized domain rewrite.\n- Treat `@Stable` as a contract that can itself be wrong, not as proof of stability or performance.\n- Strong Skipping is enabled by default with modern Kotlin/Compose compiler versions, so an\n  unstable parameter is not automatically a performance defect. Check identity churn, actual\n  recomposition evidence and compiler/benchmark data. A missing project-required annotation is P3\n  unless it causes a concrete architectural or runtime issue; an incorrect `@Stable` promise can be\n  P2 because it can suppress required updates.\n- Android `Flow` state should normally be collected with `collectAsStateWithLifecycle`. A\n  composition-scoped collector is not automatically lifecycle-aware merely because it uses\n  `LaunchedEffect`.\n- Review each ViewModel-originated `SharedFlow` effect by delivery semantics. Buffer capacity does\n  not make an event durable while no UI is collecting. Navigation, result delivery, payments,\n  destructive completion and other consistency-critical outcomes should normally reduce to state\n  or use an explicit acknowledgement protocol; best-effort transient feedback may remain an effect.\n- UI-only behavior should stay at the lowest UI owner. A direct user navigation callback handled by\n  the host is valid and often preferable to a ViewModel round trip; guard rapid/replayed navigation\n  with lifecycle-aware mechanisms such as `dropUnlessResumed` when the available Lifecycle version\n  supports it.\n- Insets and predictive back require ownership and runtime-path analysis; `Scaffold`, padding APIs,\n  Navigation 3 or `BackHandler` alone do not prove correctness.\n- Use `PredictiveBackHandler` when custom UI needs gesture progress; use ordinary `BackHandler` only\n  when binary interception is sufficient. When Navigation 3 transitions are customized, inspect\n  `predictivePopTransitionSpec` as well as normal pop behavior.\n- Reusing a project component is preferred only when its behavior, semantics and accessibility fit.\n- Prefer Material/Foundation interaction APIs because they provide semantics, focus and input\n  behavior. Custom gestures/components must preserve role, state/action semantics, keyboard/D-pad\n  access and an adequate touch target.\n\n## Current official baseline\n\nWhen library behavior matters, verify it against the repository version and current official docs:\n\n- [Strong Skipping](https://developer.android.com/develop/ui/compose/performance/stability/strongskipping)\n  and [stability contracts](https://developer.android.com/develop/ui/compose/lifecycle).\n- [Lifecycle-aware Compose collection](https://developer.android.com/topic/libraries/architecture/lifecycle).\n- [State hoisting](https://developer.android.com/develop/ui/compose/state-hoisting).\n- [UI event delivery](https://developer.android.com/topic/architecture/ui-layer/events).\n- [Material 3 Insets](https://developer.android.com/develop/ui/compose/system/material-insets)\n  and [Compose Insets](https://developer.android.com/develop/ui/compose/system/insets-ui).\n- [Predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)\n  and [Navigation 3 transitions](https://developer.android.com/guide/navigation/navigation-3/animate-destinations).\n- [Compose accessibility defaults](https://developer.android.com/develop/ui/compose/accessibility/api-defaults)\n  and [semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics).\n\nWhen implementation is requested, also read the migration skill's project patterns.\n\n<!-- File: .agents/skills/legado-kmp-migration/agents/openai.yaml (Tokens: ~68 | Category: anthropic-skill) -->\ninterface:\n  display_name: \"Legado KMP-first Migration\"\n  short_description: \"Plan shared KMP logic and independent native or Compose renderers\"\n  default_prompt: \"Use $legado-kmp-migration to plan a reviewable KMP-first slice with explicit host, renderer, interop, and verification boundaries.\"\n\n<!-- File: .agents/skills/legado-kmp-migration/references/renderer-host-strategy.md (Tokens: ~712 | Category: anthropic-skill) -->\n# Renderer and Host Strategy\n\nRead this reference when a Feature may use more than one UI stack or when a non-Kotlin host consumes\nshared Kotlin behavior.\n\n## Separate three decisions\n\n1. **Shared behavior:** domain, repositories, use cases, state snapshots, commands and reducers.\n2. **Host integration:** Gradle/JVM, Apple framework, Windows DLL/C ABI, or process/IPC.\n3. **Renderer:** Android Material/Miuix, Compose Material/Fluent, WinUI 3, SwiftUI/UIKit, or a\n   specialized platform island.\n\nDo not infer one decision from another. A KMP core does not require CMP, and Compose on one host\ndoes\nnot require every host to share the same Screen.\n\n## Renderer-neutral presentation\n\nPrefer immutable state snapshots, stable IDs, commands/intents, effects, domain values and explicit\ncapability/error states. Keep Composable lambdas, `Modifier`, Material/Fluent types,\n`StringResource`, icons/painters, navigation UI objects, Android Context, Swift/WinRT objects, Room\nentities and DI containers out of the boundary.\n\n`@Stable` belongs to a Compose-facing contract or adapter. If WinUI 3 or SwiftUI also consumes the\nstate, keep the pure state free of renderer annotations and adapt it at the Compose boundary.\n\n## WinUI 3 choices\n\nWinUI 3 cannot consume a Kotlin/JVM Gradle module like a Kotlin host. Choose the bridge explicitly.\n\n### Kotlin/Native DLL and C ABI\n\nUse a `mingwX64` shared library when in-process calls and one packaged process matter, and only when\nthe required dependency closure supports the target.\n\n- Export a small facade, not repositories, Flow, sealed hierarchies or generic Kotlin APIs.\n- Use opaque handles with explicit create/dispose.\n- Define ownership for every returned string or buffer.\n- Map async work to request IDs plus callbacks/polling; define callback thread and cancellation.\n- Version the ABI and compile a native consumer in CI.\n- Do not force JVM-only Room, Rhino or jsoup dependencies into the DLL; split a native-compatible\n  core or choose IPC for those capabilities.\n\n### JVM sidecar and IPC\n\nKeep the existing JVM data/runtime stack in a separately packaged process and expose a versioned\nlocal protocol when reuse is more important than in-process integration.\n\n- Define protocol DTOs and version negotiation.\n- Define startup, readiness, shutdown, crash recovery and upgrade behavior.\n- Restrict local access; do not unintentionally expose an unauthenticated network service.\n- Propagate cancellation and structured errors.\n- Package and test both processes as one product.\n\nUse one bounded read/write Feature to compare cold start, call latency, state mapping, database and\nruntime reuse, memory ownership, crash isolation, installer complexity and test ergonomics before\nchoosing the product-wide bridge.\n\n## iOS native renderer\n\nExport a deliberately small Apple framework facade. SwiftUI may observe a tested shared state host\nor own an `ObservableObject` that calls shared repositories/use cases. Verify Swift names,\noptionality, collections, async/Flow bridging, lifecycle, cancellation and memory ownership with a\nSwift consumer test.\n\n## Selective CMP\n\nShare a CMP Screen only when real hosts intentionally share its visual and interaction model.\nResources and design-system components belong to that renderer. If Desktop uses WinUI 3 and iOS\nuses SwiftUI, Android Compose can remain an Android renderer even when its code is technically\nportable.\n\n<!-- File: .agents/skills/legado-kmp-migration/references/slice-checklist.md (Tokens: ~843 | Category: anthropic-skill) -->\n# KMP Slice Checklist\n\nUse this reference for implementation plans, extraction work, scaffolding or reviews. The default\npath is renderer-neutral KMP. CMP checks apply only after a shared Compose renderer has been chosen;\nthey are not instructions to convert every Feature or host to Compose.\n\n## Before change\n\n- Scope names exact files/packages, current callers, one boundary and one rollback point.\n- Existing behavior is captured by tests or a written parity list.\n- The target owner, source set and consumers are real rather than copied from the target diagram.\n- Dependencies are classified as common-ready, contract-needed, renderer-specific,\n  platform-island or unknown.\n- Actual Gradle target and verification task names have been inspected.\n\n## Contract quality\n\n- Public types express domain values instead of Android/JVM/storage/rendering details.\n- Error, cancellation, threading, transaction, ordering, serialization and ownership semantics are\n  explicit where they cross the boundary.\n- Capability absence is visible to callers.\n- An ordinary interface is used unless `expect/actual` provides a concrete static benefit.\n- Every abstraction has a real caller and removes a measurable dependency.\n- Native or IPC exports are smaller and more stable than the internal Kotlin API.\n\n## Module and source-set graph\n\n- The app host owns implementation aggregation, DI, navigation/window lifecycle and packaging.\n- Core does not import Feature; Feature does not import another Feature implementation.\n- Shared modules do not depend on platform implementations or renderer modules.\n- Gradle `api` exposure is intentional; otherwise use `implementation`.\n- Package colocation, Android module extraction, KMP conversion and renderer sharing remain separate\n  changes unless evidence requires combining them.\n- `commonMain` is compiled by at least one non-Android target before being called shared.\n- JVM+Android-only sharing has an honest owner and is not mislabeled as common.\n- Platform files live in the narrowest relevant source set.\n- Android services, Context/URI/resources and notifications remain Android-side.\n- Rhino/JS behavior remains behind a capability boundary until another target has a compatible,\n  tested implementation.\n\n## Renderer and host checks\n\n- Presentation contracts contain no Compose, Material/Miuix, Fluent, WinUI, SwiftUI or platform\n  resource types.\n- A renderer owns its resources, accessibility, layout and navigation presentation.\n- Shared Compose UI emits semantic callbacks/effects; the host owns platform launchers.\n- A CMP Screen has at least two intentional consumers or another documented product reason.\n- WinUI/iOS bridges define versioning, lifecycle, cancellation, error and memory ownership.\n- A sidecar protocol defines startup, readiness, shutdown, crash recovery, upgrade and local-access\n  behavior.\n\n## Gates\n\n- Affected Android unit/lint/architecture/package gates pass.\n- Common tests and actual metadata/target compile tasks pass.\n- Capability status distinguishes compile, contract-test, smoke, package and release-ready.\n- Adapter tests cover success, failure and cancellation where relevant.\n- Serialization/database changes include backward/forward or migration evidence.\n- Reader/rule/service changes include parity and, where relevant, real-device performance evidence.\n- Reduced historical violations lower their baseline in the same change.\n- `git diff --check` passes.\n\n## Scaffolding\n\n- At least two accepted manual examples prove a convention before it is generated.\n- Dry-run is available and default execution refuses overwrites.\n- Existing graph/DI files are amended safely rather than replaced wholesale.\n- Only necessary files are generated; no empty layers or speculative targets.\n- Generator behavior has fixture, snapshot or compilation evidence.\n\n## Review output\n\nFor each finding give a tight file/line reference, concrete impact and smallest credible fix:\n\n- P0/P1: behavior/data loss, incompatible rule/storage/ABI semantics, broken target, lifecycle,\n  thread, cancellation, native ownership or packaging defects.\n- P2: illegal dependency, renderer/platform leakage, false capability, state duplication, baseline\n  relaxation or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf no issue is found, state the remaining unverified host, device, package, interop and performance\nrisks.\n\n<!-- File: .agents/skills/legado-kmp-migration/SKILL.md (Tokens: ~1586 | Category: anthropic-skill) -->\n---\nname: legado-kmp-migration\ndescription: Plan, implement, or review Legado KMP-first modularization across shared domain, data, and presentation code with independently chosen Android, Desktop, Windows-native, and optional iOS renderers. Use for Gradle boundaries, commonMain extraction, platform capabilities, native or process bridges, selective CMP, target gates, and legacy-owner removal. Do not use for an Android-only View-to-Compose rewrite unless it also changes a multiplatform or Gradle boundary.\n---\n\n# Legado KMP-first Migration\n\n## Purpose\n\nMove one verified responsibility toward a reusable KMP boundary without weakening Android behavior\nor forcing every host to use Compose. Optimize for shared behavior and explicit platform contracts,\nnot maximum shared UI percentage.\n\nValid renderers include Android Compose/View islands, Compose Desktop with Material or Fluent,\nWindows-native WinUI 3, and optional SwiftUI/UIKit. CMP is one renderer technology, not the\narchitecture root.\n\nBefore acting, read repository `AGENTS.md`, `docs/dev/kmp-cmp-modernization.md`, and the relevant\npart of `docs/dev/feature-first-structure.md`.\n\nRead supporting references only when applicable:\n\n- Implementation or review: [references/slice-checklist.md](references/slice-checklist.md).\n- Multiple renderers, WinUI 3, SwiftUI/UIKit, native export or IPC:\n  [references/renderer-host-strategy.md](references/renderer-host-strategy.md).\n\n## Choose the work mode\n\n- **Architecture/plan:** inventory current dependencies and consumers, then choose the sharing\n  boundary, renderer strategy, targets, gates and rollback point.\n- **Pure KMP extraction:** move stable models, rules, ports, use cases, reducers or stores into\n  Compose-free `commonMain`.\n- **Data/runtime extraction:** separate domain contracts from storage, network, files and rule\n  engines while retaining compatibility semantics under contract tests.\n- **Renderer boundary:** split renderer-neutral presentation from Material/Miuix, Fluent, WinUI 3,\n  SwiftUI/UIKit or specialized reader UI.\n- **Selective CMP:** share a Compose Screen/resource set only when real hosts deliberately choose\n  the same renderer and interaction model.\n- **Native host bridge:** expose a small, versioned API through an Apple framework, Windows DLL/C\n  ABI, or process/IPC boundary; do not export the internal Kotlin object graph.\n- **Build logic:** prove a target/convention/gate change with one representative module before\n  broader rollout.\n- **Review:** report findings and unverified targets first; edit only when fixes or plans were\n  requested.\n\n## Decisions required before implementation\n\n1. **What is shared?**\n    - Default candidates are domain values, business rules, repository ports, use cases,\n      serialization models, state snapshots, commands and reducers.\n    - Screens, design-system components, navigation renderers, platform ViewModel owners, resource\n      handles, lifecycle and OS integration are not shared by default.\n    - A pure presentation API must not expose Compose annotations/types, Material/Fluent types,\n      Android resources, Koin, Room, or platform SDK objects.\n\n2. **Who renders it?**\n    - Name each committed host and renderer separately; “Desktop” is not a renderer.\n    - Different renderers may consume the same presentation contract while owning distinct layouts,\n      resources, navigation and accessibility.\n    - Do not invent universal UI wrappers across unrelated design systems. Share semantic values\n      only\n      when their meaning is genuinely common.\n\n3. **How does a non-Kotlin host consume it?**\n    - JVM Compose hosts can depend on KMP/JVM modules directly.\n    - SwiftUI/UIKit needs a deliberately exported Apple framework facade.\n    - WinUI 3 needs either a Kotlin/Native `mingwX64` DLL with a narrow C ABI or a separately\n      packaged\n      JVM process reached through a versioned local IPC protocol.\n    - Keep rich Kotlin types internal. Define DTO/state snapshots, commands, errors, cancellation,\n      callback threads, allocation ownership and disposal at the bridge.\n\n4. **What evidence supports the claim?**\n    - Distinguish metadata/compile, contract-test, host smoke, package and release-ready evidence.\n    - Android plus JVM compilation does not prove iOS or Windows Native readiness.\n    - Do not document a module, target, task or host as current until it exists in this repository.\n\n## Workflow\n\n1. Bound one slice: exact files, callers, behavior, intended owner/source set, host impact and\n   rollback point.\n2. Classify dependencies as `common-ready`, `contract-needed`, `renderer-specific`,\n   `platform-island`, or `unknown`. Imports are only a first pass; compile every claimed target.\n3. Establish characterization or contract tests before moving behavior. Preserve storage, script\n   ABI, serialization, error, cancellation, ordering and threading semantics.\n4. Choose the smallest seam. Prefer interfaces plus constructor injection; use `expect/actual` only\n   for true platform primitives or a documented case where injection cannot provide the boundary.\n5. Implement additively, migrate a bounded caller set, then delete the old owner when no callers\n   remain. Do not keep internal `Help/Utils/Base/Provider` facades for import compatibility.\n6. Run actual repository gates and target tasks. Never infer task support from the target design.\n7. Report behavior evidence, dependency/baseline delta, capability changes, exact commands,\n   rollback path and unverified hosts/devices.\n\n## Boundary rules\n\n- Pure KMP domain/presentation does not depend on Compose, AndroidX ViewModel, Room entities/DAO,\n  Koin, platform resources, `File`/URI, JVM-only libraries or renderer types.\n- Renderer modules may depend on presentation; presentation never depends on a renderer.\n- Each host owns its composition root, root navigation/window lifecycle, platform effects,\n  implementation selection and packaging.\n- Unsupported capabilities are explicit; a successful no-op is not an implementation.\n- Feature-to-Feature implementation dependencies remain forbidden. Create `api/impl` only for a\n  real Gradle consumer; split renderers by responsibility instead of generic layering names.\n- Do not change storage, network, DI, navigation and UI technology in one slice.\n- Historical baselines only decrease. A new source set starts at zero and cannot justify raising a\n  baseline.\n\n## Verification minimums\n\n- Always retain the affected Android G0 gates.\n- Pure KMP: common tests plus every claimed target compile.\n- Data/runtime: adapter contracts and compatibility checks proportional to storage, serialization,\n  cancellation and error risks.\n- CMP renderer: target compile plus rendered UI/semantics smoke and runtime dependency alignment.\n- WinUI 3 DLL: native link, generated API review, consumer smoke, memory/disposal/error/thread tests\n  and package loading.\n- WinUI 3 IPC: protocol compatibility, startup/shutdown/reconnect, local-access policy,\n  cancellation and installer smoke.\n- iOS native UI: framework export plus a Swift consumer compile/observation/lifecycle smoke.\n- Always run `git diff --check`; use a clean rebuild after cross-module moves.\n\n## Review output\n\nList findings by impact with tight file/line evidence and the smallest credible fix:\n\n- P0/P1: behavior or data loss, ABI/storage incompatibility, broken target, lifecycle/thread/\n  cancellation defect, or package/runtime failure.\n- P2: renderer leakage into presentation, illegal dependency, false capability, untested bridge,\n  state duplication, baseline relaxation, or unnecessary abstraction.\n- P3: convention, naming, documentation, graph or template drift.\n\nIf there are no findings, state which host, package, device, renderer, interop and performance paths\nremain unverified.\n\n<!-- File: .codex/skills/legado-compose-migration/agents/openai.yaml (Tokens: ~57 | Category: anthropic-skill) -->\ninterface:\n  display_name: \"Legado Compose Migration\"\n  short_description: \"Guide Legado Compose screens and migration\"\n  default_prompt: \"Use $legado-compose-migration to create or migrate a Legado Android Compose screen with MainActivity navigation and standard architecture.\"\n\n<!-- File: .codex/skills/legado-compose-migration/references/project-patterns.md (Tokens: ~2794 | Category: anthropic-skill) -->\n# Legado Compose Migration Project Patterns\n\n## Existing Shape\n\n- App module: `app`.\n- Main source root: `app/src/main/java/io/legado/app`.\n- UI package: `ui/...`, usually grouped by feature.\n- Data package: `data/dao`, `data/entities`, `data/repository`.\n- Domain package: `domain/gateway`, `domain/model`, `domain/repository`, `domain/usecase`.\n- DI: `di/appModule.kt`, using Koin `singleOf`, `viewModelOf`, and parameterized `viewModel { ... }`.\n- Main navigation: `ui/main/MainActivity.kt`, using Navigation3 `NavKey`, `rememberNavBackStack`, `entryProvider`, and `NavDisplay`, plus legacy route extras such as `EXTRA_START_ROUTE`.\n- Base Compose host: `base/BaseComposeActivity.kt`, which wraps `Content()` in `AppTheme`, configures system bars, locale/font, background image, and LiveBus recreation events.\n- Compose dependencies are already enabled in `app/build.gradle.kts`; do not add new UI libraries unless the target screen truly requires one.\n- `kotlinx.collections.immutable` is available for Compose-facing screen state. Prefer `ImmutableList`, `ImmutableSet`, and `ImmutableMap` at `UiState` boundaries when collection state is passed to composables and changes often.\n\n## Files to Inspect Before Migrating\n\nInspect these as local examples, not as APIs to copy blindly:\n\n- `ui/book/info/BookInfoActivity.kt`\n- `ui/book/info/BookInfoContract.kt`\n- `ui/book/info/BookInfoViewModel.kt`\n- `ui/book/info/BookInfoScreen.kt`\n- `ui/book/search/SearchContract.kt`\n- `ui/book/search/SearchViewModel.kt`\n- `ui/book/search/SearchScreen.kt`\n- `ui/main/MainActivity.kt`\n- `ui/main/MainScreen.kt`\n- `ui/widget/components/AppScaffold.kt`\n- `ui/widget/components/list/ListScaffold.kt`\n- `ui/theme/LegadoTheme.kt`\n- `ui/theme/AppTheme.kt`\n\n## Recommended Feature Layout\n\nFor a migrated feature, prefer colocated files:\n\n```text\nui/<area>/<feature>/\n  FeatureActivity.kt       # only for legacy Intent compatibility or full-screen migration host\n  FeatureContract.kt\n  FeatureViewModel.kt\n  FeatureScreen.kt\n  FeatureSheets.kt        # only if sheets are substantial\n  FeatureDialogs.kt       # only if dialogs are substantial\n```\n\nUse the smallest set of files. Do not split files just to match this shape.\n\n## MVI and UDF Rules\n\nUse this shape for behavior-heavy screens:\n\n```kotlin\ndata class FeatureUiState(\n    val isLoading: Boolean = false,\n    val items: List<Item> = emptyList(),\n    val dialog: FeatureDialog? = null,\n    val sheet: FeatureSheet = FeatureSheet.None,\n)\n\nsealed interface FeatureIntent {\n    data object BackPressed : FeatureIntent\n    data object Refresh : FeatureIntent\n    data class ItemClick(val id: Long) : FeatureIntent\n}\n\nsealed interface FeatureEffect {\n    data object Finish : FeatureEffect\n    data class OpenDetail(val id: Long) : FeatureEffect\n}\n```\n\nViewModel rules:\n\n- Keep `_uiState = MutableStateFlow(FeatureUiState())` private and expose `uiState = _uiState.asStateFlow()`.\n- Keep `_effects = MutableSharedFlow<FeatureEffect>(extraBufferCapacity = 8)` private and expose `effects = _effects.asSharedFlow()`.\n- Prefer `fun onIntent(intent: FeatureIntent)` as the UI entry point.\n- Treat `FeatureIntent` as an MVI/user-action type. When Android `Intent` launch/extras are also involved, name variables and explanations clearly enough that the two concepts are not confused.\n- Use `_uiState.update { it.copy(...) }` for state changes.\n- Emit one-shot work through effects, not booleans in state.\n- Keep cached current entities private in the ViewModel when existing project behavior needs mutation or incremental sync, but publish immutable render state.\n- For collection-heavy `UiState`, convert DAO/repository `List`/`Set`/`Map` values to `kotlinx.collections.immutable` at the ViewModel/UI-state boundary with `toImmutableList()`, `toImmutableSet()`, or `toImmutableMap()`. Do not force repository, DAO, or domain APIs to use persistent collections unless the domain contract truly benefits.\n- Keep internal flow pipelines free to use normal Kotlin collections for sorting, grouping, and persistence work; the immutable collection rule is primarily for values exposed to Compose.\n- Use existing `BaseViewModel.execute { ... }.onSuccess { ... }.onError { ... }` when the surrounding ViewModel already uses that pattern.\n\nActivity rules:\n\n- Prefer `MainActivity` as the owner of Compose-first navigation. Add a Navigation3 route/key and `entryProvider` entry there for new destinations.\n- Extend `BaseComposeActivity` for migrated Activity screens only when a standalone Activity remains necessary.\n- Keep a legacy Activity for a migrated screen only when unreworked View code still calls it through `Intent`; in that case, make the Activity a thin compatibility host that translates extras/results and delegates to Compose state/effects.\n- Keep Activity Result launchers, file openers, clipboard, permission APIs, Android DialogFragments, and framework navigation in the Activity.\n- In `Content()`, collect state with `collectAsStateWithLifecycle().value`.\n- Collect effects in `LaunchedEffect(Unit) { viewModel.effects.collectLatest { ... } }`.\n- Pass only `state` and callbacks into the screen.\n\nNavigation rules:\n\n- For new Compose-first destinations, add the route to `MainActivity` instead of adding a new Activity.\n- Follow `MainActivity`'s current route style: serializable `NavKey` routes for in-process navigation and launcher `Intent` helper methods only when external or legacy callers need a stable entry point.\n- Keep legacy extra names stable when replacing an old Activity entry point.\n- When a compatibility Activity is retained, let it translate old `Intent` inputs/results; do not make it the source of truth for navigation or business behavior.\n\nScreen rules:\n\n- Make `FeatureScreen(state, onIntent, ...)` stateless for business state.\n- Use `remember` and `rememberSaveable` only for local UI affordances such as menu expansion, scroll state, transient animation state, and text field drafts when committing through an intent.\n- Use `rememberSaveable` for user-driven transient targets that should survive recreation, such as a delete-confirmation item ID or URL. Store only the stable ID in saved state and resolve the current entity from `UiState`.\n- Use `BackHandler` to route back actions through `FeatureIntent.BackPressed` when ViewModel decisions matter.\n- Keep expensive derived values in `remember(key)` or ViewModel state when they depend on repository data.\n- Prefer `LazyColumn`, `LazyRow`, stable keys, and existing fast-scroller/list components for adapter migrations. For heterogeneous lazy lists or grids, provide `contentType` in addition to `key`.\n- In long-lived `LaunchedEffect` collectors, wrap changing callbacks, `Context`-dependent operations, or lambdas from parents with `rememberUpdatedState` when the effect should not restart.\n- Hoist state only to the lowest common owner that reads and writes it. Move branch-only ViewModel lookup and Flow collection into the branch or a small child composable instead of collecting at a high-level route.\n- Avoid UI-state feedback loops such as `LaunchedEffect(uiState.items) { viewModel.pruneSelection(...) }`. Prefer deriving consistency inside the ViewModel with `combine(...)`, or reduce the state as part of the flow that produces the data.\n\n## Current Compose Performance Notes\n\n- Kotlin 2.x enables strong skipping by default, but ordinary Kotlin `List`, `Set`, and `Map` are still unstable to Compose. This means high-frequency screen state with normal collections can still widen recomposition work.\n- Use immutable collections for render state that crosses into Compose. Do not mechanically replace every temporary collection, Room query result, or internal mutable accumulator.\n- If data entities come from modules where the Compose compiler does not run, consider a stable UI model wrapper when the entity is passed deeply through composables and causes measurable recomposition cost.\n- Treat `SnapshotStateList` / `SnapshotStateMap` as UI-owned mutable state, not as a default ViewModel `UiState` transport type.\n\n## Clean Architecture Boundaries\n\nThis project is mid-migration, so use Clean Architecture pragmatically. For newly created screens, default to standard modern Android architecture and avoid mixed legacy patterns unless they are needed to interoperate with View screens that have not been rewritten.\n\n- For new screens, keep UI, presentation, domain, and data boundaries explicit: Compose renders state, ViewModel owns state and intent handling, usecases hold reusable business actions, repositories mediate data access.\n- For new screens, do not place business rules in Activity/Fragment or composables.\n- For new screens, do not read DAOs directly from UI/presentation unless the project already has no reasonable repository/usecase boundary and adding one would be disproportionate.\n- For migrations, use existing DAOs/repositories directly if the old ViewModel already does and the migration is UI-only.\n- Prefer existing `domain/usecase` classes for reusable business actions such as cache, delete, group update, startup maintenance, reading progress, and WebDAV flows.\n- Add a usecase when a new screen has meaningful business rules, when logic would otherwise be duplicated across features, or when a UI migration needs to extract meaningful business rules from an Activity/adapter.\n- Keep UI-only formatting and Compose layout decisions out of domain.\n- Keep entity-to-display mapping near the UI or ViewModel unless it is reused business language.\n\n## Compose Components and Theme\n\nPrefer project components before raw Material widgets:\n\n- Scaffold/top bars: `AppScaffold`, `GlassTopAppBar...`, `DynamicTopAppBar`, `TopBarActionButton`, `TopBarNavigationButton`.\n- Lists: `ListScaffold`, `TopFloatingStickyItem`, `VerticalFastScroller`, `SelectionBottomBar`.\n- Dialog/sheet: `AppAlertDialog`, `AppModalBottomSheet`, `OptionSheet`, text list input components.\n- Settings: `ClickableSettingItem`, `SwitchSettingItem`, `SliderSettingItem`, `ListSettingItem`, `SettingCard`.\n- Buttons/chips: `AppIconButton`, `SmallIconButton`, `SmallTextButton`, `ToggleChip`, `AlertButton`.\n- Covers/images: `CoilBookCover`, `BookshelfCover`, `buildCoverImageRequest`; use Coil for Compose image loading.\n- Theme: `LegadoTheme.colorScheme`, `LegadoTheme.typography`, `ThemeResolver`, `ProvideThemeOverride` where existing feature behavior needs theme override.\n\nAvoid introducing a second visual system. If a raw Material 3 component is used, style it from `LegadoTheme`.\n\n## Migration Checklist\n\nBefore editing:\n\n- Identify old XML layouts, ViewBinding fields, adapters, decorations, menu XML, dialogs, and result contracts.\n- Capture behavior: empty/loading/error states, refresh, search, sort, selection, long click, swipe, menu actions, back behavior, result codes, events, and persisted preferences.\n- Decide whether the task is UI-only or needs domain extraction.\n- For new screens, explicitly decide the presentation/domain/data boundary before coding; do not default to legacy mixed architecture.\n\nDuring editing:\n\n- Create/update `Contract`, then `ViewModel`, then `Screen`, then `MainActivity` route or compatibility host.\n- For new Compose destinations, wire navigation through `MainActivity` unless a legacy `Intent` caller must remain supported.\n- When retaining an Activity for compatibility, keep it small: parse legacy extras, collect effects, bridge result codes, and avoid placing feature business logic there.\n- Reuse existing string resources and add new resources for new user-facing text.\n- Register the ViewModel in `di/appModule.kt`.\n- Remove only resources made obsolete by this migration.\n- Keep existing public intent extra names and result codes unless explicitly changing an API.\n\nVerification:\n\n- Run `.\\gradlew.bat :app:compileAppDebugKotlin` for Kotlin-only changes.\n- Run `.\\gradlew.bat :app:assembleAppDebug` when resources, manifests, XML deletion, or generated bindings are affected.\n- If a screen uses Room queries or migration-sensitive repositories, run the relevant existing Android/unit tests if practical.\n- For manual verification, open the migrated screen and compare core flows against the old behavior list.\n\n## Common Pitfalls\n\n- Do not put navigation or Activity launchers inside composables.\n- Do not store one-time navigation as nullable fields in `UiState` unless the existing local pattern already does; prefer `SharedFlow` effects.\n- Do not keep adapter selection state split between old adapter and new Compose state.\n- Do not delete XML/menu resources until all references and generated binding imports are gone.\n- Do not add speculative domain layers for a single migrated screen.\n- Do not use mixed architecture for new screens unless compatibility with unreworked View UI requires it.\n- Do not make a migrated screen's retained Activity the primary navigation model when `MainActivity` can own the Compose destination.\n- Do not bypass `BaseComposeActivity` unless the screen must remain inside a Fragment or legacy host.\n- Do not replace existing event bus or service interactions as part of a UI migration unless required for correctness.\n\n<!-- File: .codex/skills/legado-compose-migration/SKILL.md (Tokens: ~1432 | Category: anthropic-skill) -->\n---\nname: legado-compose-migration\ndescription: Guide Legado Android UI migration from XML/View/RecyclerView/DialogFragment screens to Jetpack Compose and Material 3, and guide new Compose-first screens using standard Android architecture. Use when Codex is asked to create, migrate, rewrite, review, or plan a Legado screen, Activity, Fragment, dialog, adapter, navigation destination, or settings page, especially when MainActivity navigation, MVI/UDF, StateFlow, Koin ViewModels, domain/usecase boundaries, or existing Compose component conventions matter.\n---\n\n# Legado Compose Migration\n\n## Overview\n\nMigrate one UI surface at a time, or create one new Compose destination at a time. Preserve behavior first for migrations; for newly created screens, prefer standard modern Android architecture over mixed legacy patterns. Use the project's existing `MainActivity` navigation, `BaseComposeActivity` compatibility hosts, `*Screen`, `*Contract`, `*ViewModel`, `StateFlow`, `SharedFlow`, Koin, theme, and widget patterns.\n\nBefore editing, inspect the target View implementation if one exists, `MainActivity` navigation when adding a destination, and at least two nearby migrated Compose screens. For concrete project patterns, including current Compose state/performance rules, read `references/project-patterns.md`.\n\n## Workflow\n\n1. State assumptions and success criteria.\n   - Name the exact screen or destination being created/migrated.\n   - For migrations, define behavior that must remain unchanged: inputs, result codes, navigation, menu actions, dialogs/sheets, list selection, refresh, persistence, and event bus behavior.\n   - For new screens, define the route owner, UI state, events/effects, domain/usecase boundary, and verification target.\n   - If the target mixes UI and business logic heavily, keep the migration surgical and defer deeper domain cleanup unless needed.\n\n2. Map the old surface.\n   - For migrations, read the Activity/Fragment, XML layouts, adapters, menu XML, dialogs, result launchers, and ViewModel.\n   - For new screens, read `MainActivity`, nearby route screens, the relevant ViewModel/usecase/repository patterns, and shared UI components.\n   - List UI state, user intents, one-shot effects, and external side effects.\n   - Identify reusable Compose components under `ui/widget/components` before creating new components.\n\n3. Choose the minimal migration shape.\n   - For new Compose-first screens, add a `MainActivity` navigation destination instead of creating a standalone Activity.\n   - Use `BaseComposeActivity` for full-screen Activity migrations only when a legacy Activity must remain as an entry point.\n   - Keep existing Activity Result APIs, `Intent` extras, permission flows, and Android framework calls in the host/compatibility Activity.\n   - If an unreworked View screen still starts the migrated screen with `Intent`, keep the old Activity only as a compatibility host that parses legacy extras and delegates to the Compose screen or `MainActivity` route boundary.\n   - Put renderable state in `UiState`, user actions in `Intent`, and one-shot navigation/framework work in `Effect`.\n   - For new screens, use standard Android/Compose architecture: UDF/MVI-style state hoisting, lifecycle-aware Flow collection, ViewModel-owned state, repository/usecase boundaries, and UI free of business logic.\n   - Use mixed legacy patterns only as an integration boundary for unreworked View screens or existing framework contracts.\n   - Use existing repositories/usecases when they already fit; introduce new domain/usecase classes when a new screen needs clean business boundaries or when a migration would otherwise duplicate or entangle business logic.\n\n4. Implement by layers.\n   - Create or update `FeatureContract.kt` first for `UiState`, `Intent`, `Effect`, dialog/sheet models, and menu action enums.\n   - Update `FeatureViewModel.kt` to expose `uiState: StateFlow<UiState>` and `effects: SharedFlow<Effect>`, with a single `onIntent(...)` entry point unless the existing feature has a simpler established pattern.\n   - Create `FeatureScreen.kt` as a stateless route-level composable: `state`, callbacks, and `onIntent`.\n   - Wire new Compose destinations through `MainActivity` route handling; update a retained Activity only when legacy `Intent` compatibility is required.\n   - Collect state with `collectAsStateWithLifecycle()` and collect effects in `LaunchedEffect(Unit)` from the route or compatibility host.\n   - Register new ViewModels in `di/appModule.kt` with `viewModelOf(::FeatureViewModel)` unless parameters require `viewModel { ... }`.\n\n5. Remove only obsolete migration artifacts.\n   - Delete XML layouts, adapters, menu resources, binding fields, and imports only when the migrated screen no longer references them.\n   - Do not refactor unrelated View screens or shared utilities.\n\n6. Verify.\n   - Prefer the smallest Gradle check that compiles the touched app code, usually `.\\gradlew.bat :app:compileAppDebugKotlin`.\n   - If resources, manifests, or XML deletion are involved, run `.\\gradlew.bat :app:assembleAppDebug` when feasible.\n   - For behavior-heavy changes, add or update focused tests only where the project already has a practical test seam.\n\n## Boundaries\n\n- Keep Compose functions side-effect-light. Use `LaunchedEffect` for collecting effects and use callbacks for user actions.\n- Do not pass `Activity`, `View`, binding objects, or mutable domain entities deep into composables unless an existing local pattern requires it.\n- Prefer project theme/components: `LegadoTheme`, `AppScaffold`, `ListScaffold`, `AppAlertDialog`, `AppModalBottomSheet`, `RoundDropdownMenu`, top bar helpers, setting items, cover components, and list utilities.\n- Prefer `StateFlow`/`SharedFlow` over `LiveData` for newly migrated Compose surfaces.\n- When using `FeatureIntent` for MVI user actions, distinguish it from Android `Intent` extras and launch APIs in names, comments, and explanations where both appear.\n- For new Compose-first screens, do not copy View-era shortcuts such as UI logic in Activity/Fragment, direct binding-like mutable UI state, adapter-owned state, or Activity-context business operations.\n- Keep one-off Android actions out of `UiState`: navigation, file opening, clipboard, dialogs implemented as Android DialogFragments, result launchers, permission requests, and callbacks that require host context should be `Effect`s handled by `MainActivity` route handling or a compatibility Activity.\n- Prefer `MainActivity` navigation for new Compose destinations. Treat standalone Activities for migrated screens as legacy entry points only when existing View code still depends on `Intent` navigation.\n- Keep existing Chinese string resources and localization behavior; add strings to resources when user-facing text is new.\n\n## Reference\n\nRead `references/project-patterns.md` when implementing or reviewing a migration. It contains project-specific examples, file placement rules, state/effect conventions, and verification commands.\n\n<!-- File: .codex/skills/legado-compose-review/agents/openai.yaml (Tokens: ~62 | Category: anthropic-skill) -->\ninterface:\n  display_name: \"Legado Compose Review\"\n  short_description: \"Review Legado Compose architecture quality\"\n  default_prompt: \"Use $legado-compose-review to review a Legado Compose screen for MVI, UDF, Clean Architecture, navigation, and project convention issues.\"\n\n<!-- File: .codex/skills/legado-compose-review/references/review-checklist.md (Tokens: ~1622 | Category: anthropic-skill) -->\n# Legado Compose Review Checklist\n\n## Files to Read\n\nFor a feature review, inspect the smallest complete slice:\n\n- `FeatureScreen.kt` and any `FeatureSheets.kt` / `FeatureDialogs.kt`.\n- `FeatureViewModel.kt`.\n- `FeatureContract.kt` if present.\n- Host route in `MainActivity.kt` or retained `FeatureActivity.kt`.\n- Koin registration in `di/appModule.kt`.\n- Repositories/usecases used by the feature.\n- Old View caller or XML/adapters only when still used for compatibility or behavior comparison.\n\nUse current examples as references:\n\n- `ui/main/MainActivity.kt` for Navigation3 route ownership.\n- `ui/book/info/BookInfoContract.kt`, `BookInfoViewModel.kt`, `BookInfoScreen.kt` for behavior-heavy MVI/UDF.\n- `ui/book/search/SearchContract.kt`, `SearchViewModel.kt`, `SearchScreen.kt` for route-level search state.\n- `ui/widget/components/...` and `ui/theme/...` for shared UI conventions.\n\n## Architecture Checks\n\nFlag issues when:\n\n- A new Compose-first screen is implemented as a standalone Activity instead of a `MainActivity` destination without a compatibility reason.\n- A retained Activity contains feature business logic instead of acting as a thin Android `Intent` compatibility host.\n- Business rules, repository calls, DAO calls, persistence writes, or service orchestration live in composables.\n- ViewModel exposes mutable state directly, exposes `MutableStateFlow`, or lets UI mutate domain objects.\n- UI state is split across Activity fields, composable `remember` state, adapter state, and ViewModel state in a way that can diverge.\n- One-shot events such as navigation, result codes, file opening, permission requests, or clipboard writes are represented as persistent `UiState` fields that can replay incorrectly.\n- MVI `FeatureIntent` user actions are confused with Android `Intent` launch/extras.\n- Domain/usecase boundaries are bypassed in new screens when a meaningful business action exists.\n- A migration introduces new domain abstractions for a single trivial UI action without reducing real complexity.\n\n## UDF and State Checks\n\nFlag issues when:\n\n- `Screen` functions own business state instead of receiving `state` and callbacks.\n- `remember` / `rememberSaveable` stores source-of-truth data that should survive process or route recreation through ViewModel state.\n- User-driven transient state that should survive recreation, such as a pending delete-confirmation target, is held with plain `remember` instead of `rememberSaveable` or ViewModel state.\n- `LaunchedEffect` keys are unstable or cause repeated data loading, duplicate navigation, duplicate toasts, or repeated service calls.\n- A long-lived `LaunchedEffect` collector captures parent callbacks or context-dependent values that can change without using `rememberUpdatedState`, unless the effect is intentionally keyed to restart.\n- Flows are collected without lifecycle awareness in UI routes where `collectAsStateWithLifecycle()` should be used.\n- List items lack stable keys where mutation, selection, or animation can make state attach to the wrong row.\n- Heterogeneous lazy lists or grids provide `key` but omit `contentType`, reducing composition reuse quality when headers, rows, ads, loading items, or expanded content mix in the same lazy layout.\n- Selection, search query, sorting, filtering, loading, and error states are not represented in a single coherent `UiState`.\n- Derived values are recomputed expensively on every recomposition instead of living in ViewModel state or `remember(key)`.\n- UI effects write list-derived consistency back into the ViewModel, such as pruning selection from `LaunchedEffect(uiState.items)`. Prefer deriving this in the ViewModel with `combine(...)` or reducing it when the data flow updates.\n- State is hoisted higher than the lowest common owner, such as a top-level route collecting a child ViewModel flow only needed inside a conditional branch or one popup menu.\n\n## Compose Stability and Collection Checks\n\nFlag issues when:\n\n- Collection-heavy `UiState` exposed to Compose uses ordinary Kotlin `List`, `Set`, or `Map` in hot paths. Kotlin 2.x strong skipping is helpful but does not make standard Kotlin collections stable to Compose.\n- DAO or repository lists are passed directly through `UiState` to deep composables without conversion to `ImmutableList` / `ImmutableSet` / `ImmutableMap` or a stable UI model wrapper.\n- Review recommendations imply replacing every internal collection. Keep the finding scoped to Compose-facing render state; temporary accumulators, sorting inputs, Room DAO signatures, and domain APIs can remain normal collections unless they are the actual recomposition boundary.\n- Mutable collections such as `ArrayList`, `MutableList`, or mutable maps are stored in Compose state or ViewModel `UiState`.\n- Entity classes from non-Compose modules are passed deeply through composables and cause visible recomposition churn; prefer stable UI render models if measurement or code shape shows this matters.\n\n## Navigation and Compatibility Checks\n\nFlag issues when:\n\n- New Compose destinations skip `MainActivity` Navigation3 route registration.\n- Legacy Android `Intent` extras or result codes change without an explicit compatibility plan.\n- A migrated screen is reachable both through `MainActivity` and a retained Activity with inconsistent state initialization.\n- Navigation is performed directly inside nested composables instead of through callbacks/effects.\n- Activity Result launchers, file pickers, permission requests, or Android DialogFragments are hidden inside reusable UI composables.\n- Back behavior bypasses ViewModel decisions when unsaved changes, selection mode, add-to-shelf prompts, or confirmation dialogs exist.\n\n## UI and Project Convention Checks\n\nFlag issues when:\n\n- Raw Material components ignore `LegadoTheme` or existing shared components where the project already has a wrapper.\n- A screen recreates shared UI already present under `ui/widget/components`.\n- User-facing text is hardcoded instead of using string resources, except for temporary debug-only text.\n- Compose UI keeps ViewBinding, adapter, or XML assumptions after migration.\n- Dialogs and bottom sheets use inconsistent project components when `AppAlertDialog`, `AppModalBottomSheet`, or existing option sheets fit.\n- Image loading bypasses existing Coil cover/image helpers where cover behavior, cache, SVG, or GIF handling matters.\n\n## Clean Architecture Checks\n\nFor new screens, expect:\n\n- Compose renders state and emits user actions.\n- ViewModel owns state, intent handling, and effect emission.\n- Usecases own reusable business actions.\n- Repositories mediate data access.\n- DAOs remain behind repositories unless adding a boundary would be disproportionate and local project patterns already allow direct use.\n\nFor migrated screens, allow pragmatic intermediate code only when:\n\n- It preserves behavior.\n- It is isolated behind a clear compatibility boundary.\n- It does not spread View-era assumptions into new Compose-first code.\n\n## Output Template\n\nUse this shape unless the user asks for another format:\n\n```text\nFindings\n- [P1] Title\n  file:line\n  Impact: ...\n  Fix: ...\n\nOpen Questions\n- ...\n\nNotes\n- No issues found in ... / Tests not run ...\n```\n\nWhen using Codex inline review comments, emit one directive per finding with tight line ranges:\n\n```text\n::code-comment{title=\"[P2] Keep route state in ViewModel\" body=\"...\" file=\"/absolute/path/FeatureScreen.kt\" start=42 end=45 priority=2 confidence=0.8}\n```\n\n## Verification Suggestions\n\nRecommend verification based on the reviewed change:\n\n- Kotlin-only review/fix: `.\\gradlew.bat :app:compileAppDebugKotlin`.\n- Resource/XML/manifest impact: `.\\gradlew.bat :app:assembleAppDebug`.\n- Navigation or compatibility changes: manually open both `MainActivity` route and any retained legacy Android `Intent` entry point.\n- Behavior-heavy ViewModel changes: add or run focused tests only where the project has a practical test seam.\n\n<!-- File: .codex/skills/legado-compose-review/SKILL.md (Tokens: ~865 | Category: anthropic-skill) -->\n---\nname: legado-compose-review\ndescription: Review existing Legado Jetpack Compose code for architecture, behavior, maintainability, and project convention issues. Use when Codex is asked to audit, review, inspect, evaluate, or find problems in Legado Compose screens, routes, ViewModels, contracts, dialogs, sheets, navigation, or early Compose implementations, especially for MVI/UDF, StateFlow/SharedFlow, Clean Architecture, MainActivity navigation, legacy Activity compatibility, and View-era mixed-pattern drift.\n---\n\n# Legado Compose Review\n\n## Overview\n\nReview existing Compose code before rewriting it. Focus on concrete defects, architectural drift, behavior risks, and missing verification, especially in early Compose screens that may predate the current MVI/UDF, Clean Architecture, and `MainActivity` navigation expectations.\n\nRead `references/review-checklist.md` for the project-specific checklist, current Compose state/performance checks, and severity guidance.\n\n## Workflow\n\n1. Define the review scope.\n   - Identify the exact screen, route, ViewModel, contract, or package under review.\n   - State whether the user wants review only or review plus fixes. If unclear, review first and do not edit code.\n   - Treat unrelated legacy View code as context, not as part of the review, unless it affects the Compose surface.\n\n2. Build context from code.\n   - Read the `*Screen`, `*ViewModel`, `*Contract`, host Activity/route, DI registration, and any repositories/usecases used by the feature.\n   - Read the old View implementation only if it is still a compatibility caller or behavior reference.\n   - Compare against nearby current examples such as `BookInfo*`, `Search*`, `MainActivity`, and shared components under `ui/widget/components`.\n\n3. Review by risk, not style preference.\n   - Prioritize behavior regressions, state duplication, lifecycle bugs, navigation bugs, business logic in UI, direct data access from UI/presentation, recomposition hazards, and missing compatibility handling.\n   - Flag style-only issues only when they conflict with established project conventions or make future migration harder.\n   - Do not require broad refactors for a small screen unless the current code creates real behavior or maintenance risk.\n\n4. Output findings first.\n   - Use code-review style: list findings before summaries.\n   - Include file and tight line references.\n   - Explain the concrete impact and the smallest credible fix.\n   - If using Codex review directives, emit one `::code-comment{...}` per finding.\n   - If no issues are found, say that clearly and mention remaining test/manual verification gaps.\n\n5. Suggest fixes only after findings.\n   - Group fixes into small, reviewable steps.\n   - For architecture drift, separate compatibility-preserving fixes from larger cleanup.\n   - Recommend `legado-compose-migration` only when the finding implies a migration or rewrite workflow.\n\n## Review Priorities\n\n- **P0/P1**: behavior breakage, lost navigation/result behavior, unsafe lifecycle collection, stale state, data corruption, crashes, or compatibility entry points that no longer work.\n- **P2**: architecture drift that will cause duplicated logic, hard-to-test behavior, recomposition bugs, or incorrect ownership of state/effects.\n- **P3**: convention drift, maintainability issues, weak naming, missing previews/tests, or small cleanup that should not block behavior.\n\n## Boundaries\n\n- Do not rewrite code during a review unless the user asks for fixes.\n- Do not demand pure architecture where a compatibility boundary is necessary for unreworked View screens.\n- For new Compose-first code, expect standard Android architecture: `MainActivity` route ownership, ViewModel-owned state, UDF/MVI user actions, `StateFlow`/`SharedFlow`, repositories/usecases, and UI without business logic.\n- For migrated code, allow a thin retained Activity only for Android `Intent` compatibility with unreworked View callers.\n- Distinguish MVI `FeatureIntent` user actions from Android `Intent` launch/extras when both appear.\n\n## Reference\n\nRead `references/review-checklist.md` for detailed checks. When a review turns into implementation, also read `../legado-compose-migration/references/project-patterns.md` if available.\n</yakaai_skills>"}