by mc
Find notes that were most recently synced into your vault by Syncthing or other third-party tools, and open them in one tap. Sort by arrival, created or modified time, size, or pinyin initials.
Latest Arrivals
Find the notes that most recently arrived in your vault, and open them in one tap.
If you sync your vault to your phone with Syncthing, Resilio or anything similar, this answers a simple question: which notes just arrived? Note that it is not the same as Obsidian's own recent-files list, which remembers what you opened — this remembers what arrived.
Built for synced vaults and tuned for Android. It exists because plugins that show "recently added files" rely
on Obsidian's create event, so they never notice files that appear while Obsidian is closed — which is exactly
when a sync tool does its work.
| Plugin ID | latest-arrivals |
| Sort keys | Arrival time · file name (pinyin initials) · created time · modified time · size |
| Interface language | English, 简体中文, 繁體中文 — follows Obsidian by default |
| Bundle | main.js ~81 KB (gzip ~21 KB), zero runtime dependencies |
| Platform | Android, iOS, and desktop (isDesktopOnly: false) |
| Minimum Obsidian | 1.8.7 |
| Network | None. No requests, no telemetry, no self-updating |
1. Why other "recently added" plugins miss synced notes
Plugins such as Recently Added Files listen only to
Obsidian's create event, and register that listener after the workspace is ready. Files written by an external
sync tool arrive while Obsidian is closed, so they are picked up by the startup scan and never fire an event.
They are silently missed.
This plugin does not depend on event timing: every refresh performs a full-vault set difference.
Relationship to Recently Added Files. This project is not a fork and contains no code from it. Its source was read only to diagnose the cause above, and credit is due for that starting point.
2. How it works
The plugin keeps a table of path → first-observed time. Where that table lives is the crux:
| Storage | Synced between devices? | |
|---|---|---|
data.json in the plugin folder | Yes | ❌ devices overwrite each other |
App.saveLocalStorage() | No | ✅ device-local, per vault |
Every refresh:
- Fast path —
vault.getMarkdownFiles()plus in-memoryTFile.stat. No I/O, milliseconds. - Deep path — an optional recursive
adapter.list, to catch files Obsidian has not indexed yet. - Reconcile — known paths keep their frozen arrival time; a vanished path whose replacement has an identical
sizeandmtimeis treated as a rename and inherits it; anything else is a new arrival.
Arrival time is the default sort key because it is the only signal that does not depend on file attributes.
An external sync tool preserves mtime, so mtime really means "when you wrote the note elsewhere", and
ctime is unreliable on Android.
A newly discovered file therefore uses max(ctime, mtime) when that falls in the window since the last scan,
and is otherwise recorded as arriving now — so a freshly synced note always sorts to the top.
3. Permissions and data access
Obsidian's directory asks plugins to disclose what they touch. This plugin:
| Behaviour | Why |
|---|---|
Enumerates the vault (vault.getMarkdownFiles(), plus an optional recursive adapter.list) | It has to see the full set of notes in order to compute "what is new". The deep scan exists to catch files Syncthing has written but Obsidian has not indexed yet, and can be turned off in settings. |
| Reads and writes vault files | Reads notes to open them; writes only the optional configuration export at the vault root. |
| Clipboard | None. The plugin neither reads nor writes the clipboard. |
Device-local key/value storage (App.saveLocalStorage) | Stores the arrival ledger and a settings backup. Never synced. |
| Network | None. No network requests, no telemetry, and the plugin does not update itself or its dependencies. |
| Access outside the vault | None. No Node/Electron APIs, no adapter.getFullPath(). The deep scan starts at the vault root and only ever descends into directories it listed itself. |
4. Features
Sorting — five keys, ascending or descending
File name (pinyin initials) · Arrival time · Created time · Modified time · Note size
Pinyin sorting is implemented with zero dependencies. The JavaScript runtime's ICU already ships Chinese collation, so the plugin binary-searches a small table of syllable-boundary characters to find a character's initial, then applies a sub-1 KB override table for polyphonic characters. Verified results:
知识管理→ZSGL 本地部署→BDBS 微信→WX 数学→SX 法律→FL 经济→JJ
物理→WL 化学→HX 中国→ZG 北京→BJ 上海→SH 长沙→CS 厦门→XM
At startup the plugin self-tests whether Intl.Collator('zh') really has pinyin data. If the device's WebView
lacks it, sorting degrades to code-point order and says so in the settings, rather than silently producing
the wrong order.
Entry points
| Entry | Notes |
|---|---|
| Sidebar tab (recommended) | Mounted automatically; position (left / right / off) is configurable |
| Command palette | Latest Arrivals: Open latest arrivals |
| Quick Action (mobile) | Point it at Open latest arrivals and a pull-down from the top of the screen opens the list |
| Editor toolbar (mobile) | Settings → Interface → Toolbar → add the command |
| File context menu | Open "Latest arrivals" list · Ignore from latest arrivals |
The quick list shows 1–10 notes (configurable). Tap a row to open it; long-press for the context menu.
On mobile there is no ribbon to configure. Obsidian's mobile app has no ribbon settings page (searching settings for "Ribbon" finds nothing), which is why the sidebar tab and the Quick Action are the recommended entry points there.
Excluding folders
Settings → Latest Arrivals → Scanning → Excluded folders → Choose folders…
A searchable checkbox list of every folder in the vault. Tick one and that folder and everything under it is left out of every list — no glob syntax required.
The matcher is anchored (^Folder/), so excluding Archive does not affect MyArchive/.
A free-text Additional exclusion rules box remains for wildcard cases such as Archive/20??/*.
Configuration backup: export and import
Settings → Latest Arrivals → Configuration backup
| Button | Effect |
|---|---|
| Export | Writes the current settings to latest-arrivals-settings.json at the vault root |
| Import | Pick a JSON file from the vault and apply it (validated first; unknown keys are ignored) |
Writing the file into the vault is deliberate: it syncs to your other devices, so you can export on your computer and import on your phone, or vice versa. Import only overwrites keys that appear in the file.
Settings self-healing
Settings live in the plugin folder's data.json, which is Obsidian's convention — but that file sits inside
the plugin directory, so manually deleting and re-copying that folder takes your settings with it.
The plugin therefore also mirrors settings into device-local storage (the same place as the ledger, outside
the plugin folder). If data.json goes missing, the settings are restored from the backup on startup, with a
notice telling you so.
5. Interface language
The plugin is English by default and follows your Obsidian interface language automatically. You can also pin it under Settings → Latest Arrivals → Interface language.
| Language | Status |
|---|---|
| English | Default, and the fallback for any missing key |
| 简体中文 | ✅ |
| 繁體中文 | ✅ |
Command names, the ribbon tooltip, and the view title are registered when the plugin loads, and Obsidian has no API to rename them. Changing the language therefore reloads the plugin; if that fails, you'll be asked to toggle it off and on manually.
Adding a language means one new file in src/i18n/ plus one line in LOCALES. npm test asserts that every
bundle has an identical key set and identical {placeholder} usage.
The
nameanddescriptioninmanifest.jsonare read directly by Obsidian and cannot vary by language, so the plugin list always shows the English name.
6. Installation
First, check whether it is in the directory yet
Settings → Community plugins → Browse, and search for Latest Arrivals.
| Result | What to do |
|---|---|
| You can find it | Install it from there. This is the simplest way, and Obsidian will keep it updated for you. |
| You cannot find it | It is still in review. Use one of the two methods below. |
The review only needs to pass once. After that the directory entry stays, and new releases reach users through Obsidian's own updater — no BRAT, no manual copying.
While it is still in review — option A: BRAT
BRAT installs a plugin straight from a GitHub repository and updates it for you.
- Install the BRAT community plugin (
obsidian42-brat). - Run BRAT: Add a beta plugin for testing from the command palette.
- Enter:
iwannaknow2000/obsidian-latest-arrivals - Enable Latest Arrivals under Settings → Community plugins.
To update later, run BRAT: Check for updates to all beta plugins.
If BRAT says "This does not seem to be an obsidian plugin", the download was blocked — usually GitHub rate limiting. Turn off any VPN and try again, or use option B.
While it is still in review — option B: manual copy
This needs no network access at all.
- Download
main.js,manifest.json, andstyles.cssfrom the latest release. - Copy them into
<your vault>/.obsidian/plugins/latest-arrivals/. - Fully quit and reopen Obsidian, then enable the plugin under Settings → Community plugins.
Overwrite the three files; do not delete the folder —
data.jsonlives there. (And if you do delete it, the settings backup described above restores your configuration.)
How updates work, by install method
| Installed via | How to update |
|---|---|
| Official directory | Settings → Community plugins → Check for updates — works once listed |
| BRAT | Settings → BRAT → Check for updates to all beta plugins |
| Manual copy | Re-copy the three files and restart Obsidian |
Until the directory entry is approved, the built-in "Check for updates" button cannot see this plugin — it only compares against the official directory. That is by design, not a fault.
7. Development
npm install
npm run dev # watch build (writes main.js with an inline sourcemap)
npm test # 54 logic assertions + 29 runtime smoke assertions
npm run lint # official rule set, with type-checked rules
# (the directory's source-code review runs the same set)
npm run build # type-check + production build
npm run release:dry # show what a release would do, without changing anything
npm run release # bump version, commit, tag, push → CI builds and publishes
Layout
src/
├── main.ts Plugin entry: commands, ribbon icon, sidebar tab, settings, foreground rescan
├── ledger.ts Arrival ledger (device-local persistence, reconciliation, rename inheritance)
├── scanner.ts Fast path (vault index) + deep path (recursive adapter traversal)
├── service.ts Data core: scan → reconcile → sort → expose to the UI
├── sort.ts Five sort keys + exclusion matchers
├── settings-io.ts Configuration export and import
├── pinyin.ts Syllable-boundary binary search + polyphone table + capability self-test
├── format.ts Byte and time formatting
├── settings.ts Settings tab + diagnostics panel
├── types.ts Shared types and defaults
├── i18n/ en / zh-CN / zh-TW bundles
└── ui/ Modal, sidebar view, row renderer, folder picker
test/
├── run.ts Logic assertions (bundled by esbuild, run by Node)
├── smoke.cjs Runtime smoke test (stubbed Obsidian, loads the real build output)
└── stub/ Minimal Obsidian API stub
Test coverage
Logic (test/run.ts, 54 assertions): pinyin initials and polyphones; key-set and placeholder parity across
all three language bundles; ascending and descending order for all five sort keys; exclusion-folder anchoring,
escaping and false-positive checks; ledger baseline, idempotent rescans, frozen firstSeen, incremental
discovery, rename inheritance, delete-and-restore, arrival-window clamping, persistence round-trip.
Runtime smoke (test/smoke.cjs, 29 assertions): loads the actual build output with a stubbed Obsidian module
and runs onload() end to end — command, view and ribbon registration, automatic sidebar mounting (including
position and "does not steal focus"), sidebar relocation and deduplication, all five sort keys, the ledger
landing in device-local storage, incremental discovery, and ignoring.
8. Known limitations
- No network access at all. No requests, no telemetry, no self-updating.
- Polyphonic characters: only the roughly 40 characters whose readings differ in initial letter are overridden (长, 厦, 传, …). A complete dictionary costs several hundred KB, which conflicts with staying light.
- Deep scan cost:
adapter.listis one bridge call per directory. On a vault of a few thousand notes this takes 1–2 seconds, so it is throttled to once every 180 s by default. It can be tuned or disabled. - Files not yet indexed by Obsidian: the deep scan can detect them and record their arrival time, but opening one prompts you to restart Obsidian. After a restart it appears in its normal place.
data.jsonis synced between devices (settings), while the arrival ledger is not (it lives in device-local storage), so each device keeps its own arrival times.- Changing the language re-keys the ribbon item. Obsidian builds a ribbon item's ID from its title, so switching the interface language can drop the plugin from a configured ribbon menu. Pick your language first.
- Settings do not appear in Obsidian's settings search. The official lint suggests adopting
getSettingDefinitions(), the declarative settings API introduced in Obsidian 1.13.0. This plugin'sminAppVersionis 1.8.7, and using a 1.13-only API would lock out every user on an older build, so it deliberately keepsdisplay(). The trade-off is recorded ineslint.config.mjs.
License
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.