This file provides guidance to coding agents when working with code in this repository.
CLAUDE.md and AGENTS.md describe the same repository-level rules for different coding
agents. Any change to repository guidance must update both files in the same change and keep
their instructions semantically equivalent. Do not modify only one of these files.
npm install # Install dependencies
npm run prebuild # Generate native Android/iOS projects
npm run android # Build & run on Android device/emulator
npm run ios # Run on iOS (if supported)
npm run build:apk # Build release APK (android/gradlew assembleRelease)
npm run plugin:build # Compile Expo config plugins (TypeScript in plugins/)
npm run type-check # tsc --noEmit
npm run lint # ESLint (TS/TSX/JS/JSX)
npm run lint:fix # ESLint auto-fix
npm run format-docs # Prettier for JSON/Markdown
npm run test # Run all Jest tests
npm run test:watch # Jest watch mode
npm run test:coverage # Jest with coverage
npx jest <file> --no-coverage # Run a single test file (e.g., src/__tests__/xxx.test.ts)Before considering a task complete, run every validation command that applies to the files changed in the task:
- After modifying TypeScript or JavaScript files (
.ts,.tsx,.js, or.jsx), runnpm run lint:fixand thennpm run type-check. Run the type check after lint fixes so it validates the final code. - After modifying Expo config plugin implementation files under
plugins/, or their TypeScript build configuration, runnpm run plugin:buildafter linting. - After modifying application/runtime behavior or tests, run
npm run test. This includes changes to screens, hooks, services, stores, tasks, utilities, native-module JavaScript/TypeScript APIs, and test files. Documentation-only, comment-only, and formatting-only changes do not require the test suite. - After modifying Markdown or JSON files, run
npm run format-docs. - If multiple rules apply, run all applicable commands. After any auto-fix, build, or formatting command, inspect the diff and do not include unrelated generated or formatting changes.
This is a React Native clipboard synchronization app built with Expo SDK 55, using React Native 0.83 and React 19. It syncs clipboard content (text, images, files) between devices via SyncClipboard Server, WebDAV, or S3 backends.
Current status: Android-only. iOS support is planned for the future.
All code should be written with cross-platform portability in mind (see Cross-Platform Design Principles below). When writing code, consider iOS compatibility but do not implement iOS-specific features or native modules right now — just avoid patterns that would make iOS support unnecessarily difficult later.
index.ts registers three separate RN roots, each serving a distinct purpose:
| Registration | Component | Purpose |
|---|---|---|
main |
App.tsx |
Full app UI with bottom tab navigation (Home / History / Settings) |
quickAction |
src/QuickActionApp.tsx |
Transparent overlay for quick-settings tiles, share menu, and text selection actions |
serviceRestart |
src/ServiceRestartApp.tsx |
Brief "service restored" screen shown when Android restarts the process |
| Headless | src/tasks/SmsUploadTask.ts |
Background SMS verification code upload (no UI) |
App.tsx handles cold/hot start deep link routing: parses syncclipboard:// URLs to determine whether to show the main UI or an overlay (share receive, quick upload/download).
Zustand stores in src/stores/:
useSettingsStore— central app config (servers, sync settings, clipboard access methods, UI preferences). Persisted viaConfigService→AsyncStorage.useLocalClipboardStore— current local clipboard contentuseHistoryStore— clipboard history itemsuseMessageStore/useErrorStore— toast messages and errors (UI-only, not persisted)useClipboardSyncServiceStore(insrc/serviceState/) — sync operation state (progress, remote content, upload/download flags)
Key singletons, typically accessed via getInstance() or module-level exports:
ConfigService(src/services/ConfigService.ts) — the single source of truth for persisted app configuration. Reads/writesAsyncStorage. Has a subscriber pattern for change notification.ClientFactory(src/services/ClientFactory.ts) — creates the appropriate API client from the active server config. Supports three backends:SyncClipboardClient— custom server (HTTP + optional SignalR)WebDAVClient— WebDAV storageS3Client— S3-compatible object storage Clients implementISyncClipboardAPI(insrc/api/clients/APIClient.ts), which usesaxios+AuthService.
BackgroundRuntimeState(src/services/BackgroundRuntimeState.ts) — non-persisted boolean flag (isTempDisabled) for temporarily disabling background tasks. Decoupled from Zustand to avoid circular dependency betweenLongRunningTaskManagerandsettingsStore.ClipboardMonitor(src/services/clipboard/ClipboardMonitor.ts) — polls the system clipboard for changes usingnative-timer(notsetInterval, to survive background). Detects changes via hash comparison. Accepts background-running checkers.RemoteClipboardMonitor(src/services/sync/RemoteClipboardMonitor.ts) — monitors the remote clipboard. Uses SignalR for SyncClipboard servers, polling for WebDAV/S3. Has deduplication viaDedupedOperationand content hash tracking.ClipboardSyncService(src/services/sync/ClipboardSyncService.ts) — wires together local clipboard monitoring, remote monitoring, and history sync. Subscribes to clipboard changes, remote changes, history changes, and transfer queue events.
src/longRunningTask/LongRunningTaskManager.ts is the central lifecycle manager for all background tasks. Each task implements ILongRunningTask (src/longRunningTask/LongRunningTask.ts):
start()/stop()— idempotentisRunning()— query stateonConfigChanged()— react to config changes while runningonBackground()/onForeground()— react to app state transitions
Registered tasks (in LongRunningTaskManager.ts, at module level):
| Task | keepAlive? | Purpose |
|---|---|---|
foregroundServiceTask |
No | Manages Android foreground service notification |
clipboardSyncTask |
No | Registers callbacks in ClipboardSyncService |
heartbeatTask |
No | Periodic SignalR ping for SyncClipboard servers |
smsForwardingTask |
Yes | SMS forwarding hook |
clipboardMonitorTask |
Yes | Clipboard polling lifecycle |
remoteClipboardMonitorTask |
Yes | Remote monitor lifecycle (SignalR/polling) |
historyTrackerTask |
Yes | Local history change tracking |
historySyncTask |
Yes | History record sync with server |
networkAutoSwitchTask |
Yes | Selects servers from network changes |
keepAlive tasks ignore the "background tasks" master toggle and always run. Non-keepAlive tasks are stopped when the user disables background tasks or when the app is backgrounded with temp-disabled state.
src/utils/clipboardProxy.ts wraps clipboard access with a priority chain:
- Shizuku (
modules/shizuku-clipboard/) — system-level clipboard access via Shizuku API (no root needed if Shizuku is running) - Overlay (
modules/clipboard-overlay/) — 1px floating window that briefly focuses to read clipboard in background - expo-clipboard — default Expo clipboard API (works in foreground only)
The overlay has a 10-second idle timeout (managed via native-timer) to auto-hide and save resources.
Workspace packages, each is an Expo module with its own package.json. Currently all modules have Android native code only; iOS stubs should be added when iOS development begins.
| Module | Platform | Purpose |
|---|---|---|
signalr-client |
Cross-platform (JS) | Wraps @microsoft/signalr JavaScript client, manages connection lifecycle |
native-util |
Android | Native file operations (hash calculation, file copy, APK install). iOS: equivalent file APIs likely already exist in expo-file-system |
native-timer |
Android | Reliable background timers (survives doze mode). iOS: may not be needed (iOS timers behave differently in background) |
clipboard-overlay |
Android-only | Floating overlay window to read clipboard in Android background. iOS: no equivalent needed (iOS clipboard access in background is handled differently) |
shizuku-clipboard |
Android-only | Shizuku API clipboard access. iOS: not applicable |
sms-forwarder |
Android-only | SMS broadcast receiver + verification code extraction. iOS: not applicable (iOS doesn't allow SMS interception) |
foreground-service |
Android-only | Android foreground service notification. iOS: use background modes / BGTaskScheduler equivalent |
shortcut |
Android | Dynamic app shortcuts and quick-settings tiles. iOS: use expo-quick-actions for home screen quick actions |
TypeScript files compiled to plugins/build/ via npm run plugin:build. Listed in app.json plugins array. These modify AndroidManifest.xml, build.gradle, and resource files to add quick-settings tiles, share targets, process-text actions, foreground service permissions, Shizuku provider, and SMS receivers.
All plugins currently target Android exclusively (using Expo's withAndroidManifest, withAndroidBuildGradle, etc. modifiers). When iOS support is added, corresponding withIos* modifiers should be added to relevant plugins.
ServerConfig(api.ts) — defines a backend server (type:syncclipboard|webdav|s3, url, credentials, S3-specific fields)ClipboardContent(clipboard.ts) — unified clipboard content (type, text, fileUri, hashes, etc.)HistoryItem(clipboard.ts) — clipboard history entry with sync state, version, soft-deleteAppConfig(storage.ts) — full persisted app configurationProfileDto(api.ts) — server-side clipboard profile model (matches backend DTO)
Bottom tab navigator (Home / History / Settings) in src/navigation/AppNavigator.tsx. The settingsStore exposes many typed setter methods (e.g., setEnableClipboardOverlay, setEnableShizukuClipboard, setEnableSmsForwarding).
- i18n:
react-i18nextwithzhandenlocales insrc/i18n/locales/. Language detection viaexpo-localization. - Theme: Light/dark/auto via
ThemeContext. Colors defined insrc/theme/colors.ts. ThecreateTheme()factory resolvesautomode against system color scheme.
The app is currently Android-only, but iOS support is planned. When writing any code (JS/TS or native), follow these principles to keep the iOS path open without implementing iOS functionality today.
-
Use
Platform.OSfor branching, neverPlatform.selectwith an "android-only" fallback that would crash on iOS. Prefer this pattern:// ✅ Good — iOS gets a clean no-op or early return, Android logic is self-contained if (Platform.OS !== 'android') return; // Android-specific code here // ✅ Good — explicit default const value = Platform.select({ android: () => doAndroidThing(), default: () => fallbackForOtherPlatforms(), })(); // ❌ Avoid — iOS falls into `default` which may error const result = Platform.select({ android: androidSpecificValue });
-
Never use
import { ... } from 'react-native'APIs that are Android-only without a platform guard. (e.g.,ToastAndroid,BackHandler.exitApp()). -
Dependency injection / strategy pattern — when a feature fundamentally requires different implementations per platform (e.g., clipboard access), define a common interface in shared code and inject the platform-specific implementation. The
clipboardProxy.tsfallback chain is a good example: it already has Shizuku → Overlay → expo-clipboard, and iOS can slot into this chain naturally. -
Keep iOS as a silent no-op target — new features that cannot work on iOS today should still compile and run without crashing. The app should at minimum display the UI on iOS, even if certain features show "not available on iOS" states.
- Every native module should have an iOS stub (even a minimal
expo-module.config.json+ empty Swift/ObjC file) sopod installdoesn't fail. This keeps the iOS build compiling without implementing the feature. - Module interfaces should be platform-agnostic — the JS API exported by a module (
index.ts) should not assume Android. If the module can only work on Android, export a function that gracefully returnsnullor throws a descriptive error on iOS, rather than requiring the caller toPlatform.OS-guard every import. - Use static imports for platform-specific modules — keep imports at module scope and guard Android-only calls with
Platform.OS. The module's JavaScript entry point must remain safe to import on unsupported platforms and provide a no-op or descriptive fallback.
- Before adding a new Android-only native module or config plugin, ask: "What would the iOS equivalent look like?" If the answer is "it doesn't apply to iOS," make sure the JS layer handles absence gracefully.
- New permissions, manifest entries, or Gradle dependencies added in config plugins should be scoped with an Android platform check (the Expo plugin API provides
withAndroidManifest,withAndroidBuildGradle, etc.) — use those rather than modifying shared config.
In short: implement Android fully, but never paint yourself into an Android-only corner.
See docs/bug-fix-workflow.md for the TDD-style bug fix process:
- Reproduce & locate root cause
- Extract buggy logic into a testable pure function in
src/utils/with dependency injection - Write a failing test in
src/__tests__/that asserts the correct behavior - Fix the extracted function so the test passes
- Update callers to use the fixed function
- Run lint + type-check
- Commit
// src/utils/xxxLogic.ts
export interface XxxDeps {
getSomeState: () => SomeType;
queryStorage: (key: string) => Promise<Result | null>;
}
export async function xxxLogic(input: InputType, deps: XxxDeps): Promise<OutputType> {
// pure logic here
}- Linter: ESLint v9 flat config (
eslint.config.mjs) with TypeScript, React, React Hooks, React Native, and Prettier plugins - Import aliases: Path aliases defined in both
tsconfig.jsonandbabel.config.js:@/→src/,@components/,@services/, etc. Module aliases:native-util,shortcut,signalr-client, etc. point to theirsrc/directories. - React Native style:
react-native-no-inline-styles: warn,react-native-no-color-literals: warn - Unused vars: Warn level (
_prefix ignores) - Explicit any: Warn level — discouraged but not banned
- Module aliases: Must be kept in sync between
tsconfig.jsonpaths andbabel.config.jsaliases - File naming: PascalCase for components/screens, camelCase for stores/services/utils
- 临时 import 禁止: 严禁使用临时 import(即在函数体内或代码块中使用
require()或await import()动态导入模块)。所有 import 必须放在文件顶部、模块作用域中。临时 import 会导致模块加载不可预测、破坏 tree-shaking、增加运行时开销,并且通常是调试遗留代码。如需条件加载,应使用模块顶层的静态 import 配合Platform.OS守卫或依赖注入模式。 - Comments: JSDoc block comments used on classes, interfaces, and exported functions; comments written in Chinese on architecture-level files