Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/reference/features/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ TinyDash scans Desktop, Documents, and Downloads after the launcher opens. It us

The index contains regular files and folders under the roots. The roots themselves are not results. It excludes dot files, hidden files, Windows system files, symbolic links, and the `node_modules` and `target` folders. Hidden and excluded folders are not traversed. Configured roots must be folders and cannot be symbolic links. Overlapping roots are scanned once. Paths that cannot be represented as UTF-8 are skipped. Access errors appear as a scan warning; other folders remain searchable. TinyDash does not read file contents.

The default limit is 50,000 files and folders together. A scan also stops after visiting 500,000 entries, including folders. The UI reports a limit when the index is incomplete. Use smaller roots if a large directory reaches a limit. Scans and index preparation run on a background worker. The previous index remains searchable during a scan with the same settings. Changing file settings immediately clears the old snapshot so removed roots cannot remain searchable. Each settings change advances a generation, including changing roots back to an earlier value. Root lookup, alias resolution, traversal, and watch registration check that generation and shutdown state between operations; obsolete partial results and partially registered watches are discarded and cannot replace the current index. A cancelled cycle notifies the UI that scanning has stopped before waiting for its next budget. Cancellation cannot interrupt an OS filesystem call that is already blocked. Search uses memory, returns at most 30 results, and applies usage scores before limiting file results.
The default limit is 50,000 files and folders together. A scan also stops after visiting 500,000 entries, including folders. The UI reports a limit when the index is incomplete. Use smaller roots if a large directory reaches a limit. Scans and index preparation run on a background worker. The previous index remains searchable during a scan with the same settings. Changing file settings immediately clears the old snapshot so removed roots cannot remain searchable. Each settings change advances a generation, including changing roots back to an earlier value. Root lookup, alias resolution, traversal, and watch registration check that generation and shutdown state between operations; obsolete partial results and partially registered watches are discarded and cannot replace the current index. A cancelled cycle notifies the UI before waiting for its next budget. For the current settings, the status is queued when replacement work is pending, or disabled when no search folders are selected; an obsolete cycle cannot restore its old status or warning. Cancellation cannot interrupt an OS filesystem call that is already blocked. Search uses memory, returns at most 30 results, and applies usage scores before limiting file results.

File actions and missing pins use an expected O(1) hash-table ID lookup rather than scanning the whole index. This deliberately adds one owned ID and one table entry per indexed file. On a 64-bit host, ID bytes plus 24 bytes per table capacity slot are a lower bound; control bytes, spare buckets, and allocator overhead add more. The table is replaced and released with its index. It caches neither file contents nor complete result objects. The [synthetic search benchmark](launcher.md#synthetic-search-measurements) reports this estimate for each fixture size.

File matching uses Unicode NFC so composed and decomposed accents match. The small [unicode-normalization crate](https://docs.rs/unicode-normalization/0.1.25/unicode_normalization/) supplies canonical normalization. This fixes accented filenames returned by macOS. Display text, file IDs, and open actions keep the original path.

Operating system notifications update the file index after creation, renaming, moving, and deletion. A single worker combines a burst of changes, waiting for 300 ms of quiet or at most two seconds of continuous changes. Separately, after every worker cycle (including cancelled work), it rests for at least one second or three times that cycle's duration, whichever is longer. The measured work includes root lookup, alias resolution, watcher creation, traversal, index preparation, and watch registration. Rest starts only after that work completes; registration cannot consume the cooldown. Disposing obsolete watches during a wait also counts as work and extends the rest. This bounds sustained filesystem-work duty to 25% of work-plus-rest time; long cycles therefore delay automatic and manual refreshes beyond the settle window. Refresh requests and settings changes cannot bypass the workload budget. The worker retains only one pending request, reads the latest settings after waiting, and drops obsolete watches during the wait. There is no folder polling timer. Content-write and access events do not require filename indexing.

The launcher distinguishes **Waiting to refresh files...** from **Scanning files...**. Waiting includes burst settling and budget cooldown, even when the worker has already consumed and combined the queued signals. Existing same-settings results remain usable while waiting. A root change clears the old results but shows waiting, not a finished empty index. With no configured folders it shows **File search is off**. Only an idle, completed empty index shows **No files in the index**. A scanner startup failure shows **File scan unavailable**, preserves its warning, and can be retried with Refresh files. The internal file status uses one `phase` (`disabled`, `idle`, `queued`, `scanning`, or `failed`), rather than an additional independent indexing flag. Events announce transitions, not a countdown or repeated cooldown ticks.

Aggregate `File scan workload` logs report worker cycles as `scans`, cancellations, visited entries, total work milliseconds as `scan_ms`, and elapsed worker-window milliseconds (for cycle frequency). Preparation cancelled before traversal and cancelled registration contribute to these totals, not just completed scans. Metrics contain no full paths, filenames, queries, or error strings.

The [notify](https://docs.rs/notify/8.2.0/notify/) watcher uses FSEvents on macOS, ReadDirectoryChangesW on Windows, and inotify on Linux. Linux watches only folders accepted by the scanner, up to 8192 folders, so excluded trees do not consume recursive watches. Parent watches detect a removed or recreated search root. Registration limits or failures produce a warning. Network filesystems and restricted folders can omit events. **Refresh files** in the actions or tray menu remains available; Command/Ctrl + R refreshes files in Files mode. Set `fileWatchEnabled` to `false` for manual updates. File contents and file metadata are not stored in SQLite.
Expand All @@ -32,7 +34,7 @@ bun run test:rust -- launcher::files
bun run test:rust -- launcher::file_watch
```

Rust regressions pause a 4096-entry traversal during rapid root changes, reject an obsolete completed scan even after roots change back, and accept only the newest generation. They also cover stop/disable cancellation, same-settings snapshot retention, immediate removed-root invalidation, stale-warning rejection, aggregate counters, and workload limits under refresh floods. Worker-loop regressions delay preparation and registration to check complete-cycle accounting, post-work rest, and cancellation status notifications. Controlled watcher operations cancel at batch start, removal, addition, and commit to prove that the remaining obsolete operations are skipped; root-resolution checks cover cancellation between metadata and ancestor lookups. These are backend proofs, not integrated desktop checks.
Rust regressions pause a 4096-entry traversal during rapid root changes, reject an obsolete completed scan even after roots change back, and accept only the newest generation. They also cover stop/disable cancellation, same-settings snapshot retention, immediate removed-root invalidation, stale-warning rejection, aggregate counters, and workload limits under refresh floods. Worker-loop regressions use an injected clock at preparation, registration, rest, and settling boundaries to check complete-cycle accounting and cancellation status notifications without timing sleeps. They check queued follow-ups during scans, refresh floods consumed during cooldown, newest-root selection, disabling, stale phase/warning rejection, and retry after scanner startup failure. A final-budget-to-begin regression registers real watches, disables in that gap, observes obsolete-watch disposal before re-enabling, and checks that re-enable honors the disposal's accounted rest. Browser regressions distinguish queued, scanning, finished-empty, disabled, and failed states, including Settings-window root-change notifications; they do not simulate real filesystem timing. Controlled watcher operations cancel at batch start, removal, addition, and commit to prove that the remaining obsolete operations are skipped; root-resolution checks cover cancellation between metadata and ancestor lookups. These are backend proofs, not integrated desktop checks.

Use Files and All with an isolated root. Create, rename, and delete a file without manual refresh. Confirm the visible result changes. Open a selected file and check the handler marker. Exercise Refresh files and Ctrl/Command + R when manual refresh changes.

Expand Down
6 changes: 4 additions & 2 deletions docs/reference/features/launcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All searches applications, files, clipboard text, emoji, calculations, system co

Search keeps one expensive request in flight and replaces waiting input with the latest query. New input, hiding, or disposal sends a separate lightweight cancellation for the active request. Stale replies are still ignored. A cancellation failure does not dispatch overlapping searches; the latest waiting query follows the old response. Cancellation acknowledgements finish before the next dispatch.

A successful current search clears a previous search failure, including a background retry of the same query after a data-change event. It does not dismiss an action or startup failure. Those errors take precedence while present and keep the existing explicit dismissal paths, such as editing the query or reopening the launcher. Obsolete successes and failures cannot change the current search error.

A request has one 250 ms cooperative budget, starting in the backend command before blocking-worker queueing and including search-lock wait, providers, and pins. App/file matching checks every 64 entries, clipboard matching every 16 entries, and calculator evaluation uses its interrupt callback. The backend also checks between providers and pins and before returning a response. Expired requests report a notice/error rather than a partially ranked list. This is not a hard input-to-paint guarantee: runtime scheduling, individual matcher calls, sorting, fixed-size providers, serialization, IPC, and WebView rendering can overrun the cooperative checkpoint interval.

Tab and Shift + Tab change categories while preserving the query and search focus. Arrow keys select results. Enter runs the selected action. Escape closes a dialog or menu before hiding the launcher. [Keyboard reference](../keyboard-shortcuts.md) lists the remaining controls.
Expand All @@ -22,10 +24,10 @@ Run this browser recipe from the repository root. It retains successful traces i

```sh
bun run verify:browser tests/welcome.spec.ts tests/categories.spec.ts tests/launcher.spec.ts
bun run verify:browser tests/launcher-controller.spec.ts tests/native-subscriptions.spec.ts tests/launcher-warnings.spec.ts tests/ipc-contract.spec.ts tests/ipc-drift.spec.ts
bun run verify:browser tests/launcher-controller.spec.ts tests/search-errors.spec.ts tests/native-subscriptions.spec.ts tests/launcher-warnings.spec.ts tests/ipc-contract.spec.ts tests/ipc-drift.spec.ts
```

The standalone controller checks cover result reconciliation, refresh selection, hidden/disposed replies, reopening before an old reply settles (the original single-flight queue must ignore the old result and keep the new search pending), failure state, and browser-preview gating. Subscription checks cover late registrations and teardown. Warning tests use changed display text to prove that settings repair follows structured codes rather than English prefixes; unrelated platform warnings remain visible. A storage warning must leave search results usable.
The standalone controller checks cover result reconciliation, refresh selection, hidden/disposed replies, reopening before an old reply settles (the original single-flight queue must ignore the old result and keep the new search pending), failure state, same-query error recovery, superseded-success isolation, and browser-preview gating. Mocked-IPC browser regressions drive input and actions, then emit background events to check search-error recovery without an input edit, action/startup error survival, and stale success isolation while the latest retry is pending. These tests prove frontend ownership, not native event delivery or OS action outcomes. Subscription checks cover late registrations and teardown. Warning tests use changed display text to prove that settings repair follows structured codes rather than English prefixes; unrelated platform warnings remain visible. A storage warning must leave search results usable.

For affected backend behavior:

Expand Down
2 changes: 1 addition & 1 deletion scripts/perf/file-index/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mod launcher {
#[derive(Debug, serde::Serialize)]
pub struct FileStatus {
pub total: usize,
pub indexing: bool,
pub phase: &'static str,
pub warning: Option<String>,
}
}
Expand Down
26 changes: 23 additions & 3 deletions src-tauri/src/launcher/contract_tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
//! Canonical wire examples are produced by serde, then type-checked by TypeScript.
use super::{Action, ActionConfirmation, ResultKind, SearchResponse, SearchResult, ToolDetail};
use crate::{
launcher::{currency::CurrencyStatus, files::FileStatus, pins::ResultPin, query::SearchMode},
launcher::{
currency::CurrencyStatus,
files::{FilePhase, FileStatus},
pins::ResultPin,
query::SearchMode,
},
settings::{AppPreference, CategoryShortcut, Settings, WebSearch},
};
use serde_json::json;
Expand Down Expand Up @@ -155,7 +160,7 @@ fn serialized_ipc_contracts_match_frontend_fixture() {
storage_error: None,
files: FileStatus {
total: 3,
indexing: false,
phase: FilePhase::Idle,
warning: None,
},
currency: CurrencyStatus {
Expand All @@ -178,7 +183,7 @@ fn serialized_ipc_contracts_match_frontend_fixture() {
)),
files: FileStatus {
total: 1,
indexing: true,
phase: FilePhase::Scanning,
warning: Some("Scan incomplete".into()),
},
currency: CurrencyStatus {
Expand All @@ -187,7 +192,22 @@ fn serialized_ipc_contracts_match_frontend_fixture() {
warning: Some("Rates are old".into()),
},
};
let file_statuses: Vec<_> = [
FilePhase::Disabled,
FilePhase::Idle,
FilePhase::Queued,
FilePhase::Scanning,
FilePhase::Failed,
]
.into_iter()
.map(|phase| FileStatus {
total: 0,
phase,
warning: (phase == FilePhase::Failed).then(|| "Cannot start the file scanner".into()),
})
.collect();
let value = json!({
"fileStatuses": file_statuses,
"modes": modes, "actions": actions, "response": response, "warningResponse": warning_response,
"fullResult": full_result, "details": details, "settings": settings,
"defaults": Settings::default(),
Expand Down
Loading