Conversation
- Default file pane view is now Grid (was Details), with a one-time migration that flips existing installs still on the old default while respecting any explicit choice made afterward. - fileIcons: add distinct glyphs/colours for design files (Photoshop, Illustrator, Figma, XD, Affinity, InDesign, CorelDRAW), 3D/CAD (blend/obj/fbx/gltf/stl/…), and more languages (Haskell/Clojure/Julia/Terraform/…) — so a PSD no longer looks like a JPG. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Known file types now show their actual Material Icon Theme icon — the Photoshop mark on a .psd, the Python logo on a .py, react_ts on .tsx, docker on a Dockerfile — the same set VS Code file managers use. The prebuilt manifest drives filename/extension → icon resolution (longest-extension match, e.g. app.d.ts), and the SVGs bundle as hashed assets via import.meta.glob (offline, no network). Folders, symlinks, and unmatched files keep the lucide glyphs. MIT-licensed; notice added in THIRD_PARTY_LICENSES.md. Verified: tsc clean, vite build emits the SVG assets, and the extension→icon mapping resolves to real files (psd/ai/py/fig/docker/blend/…). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implementation-ready plan for an additive Iconify layer (offline-bundled, permissive-licence sets) giving recognizable brand/protocol logos on connections. Explicitly scoped NOT to touch file-type icons (Material Icon Theme is more complete + carries the extension mapping Iconify lacks) or the lucide UI icons. Added to ROADMAP as Track E. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cross-backend disk-usage treemap + size-ranked tree, opening as a workspace tab like the SSH terminal. Reuses the RemoteFs walk so it works on SFTP/S3/FTP/Agent/ local; the differentiator is remote servers/buckets, with a shell du/find fast path (SSH + exec-gated Agent) and object-store flat listing for scale. Canvas treemap. Added to ROADMAP as Track F. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Plan 7 Directory Diff: any-backend (incl. remote↔remote) diff surfaced in GUI, faro-cli, and as an MCP faro_diff tool; reuses sync.rs's diff. - Plan 8 Fleet Search: name+content search, exec rg/grep fast path + walk fallback, GUI/CLI/MCP. - Plan 9 Fleet Skills: reshape of multi-server runner into AI-authorable, MCP-native reusable automations, built on the bridge's saved-commands + faro_exec + approvals; safety-gated. - Plan 10 Scoped connection sharing: time-boxed, path-jailed, read-only sharing via the agent's pairing/policy/revocation (no login system); browser view deferred as 10b. - ROADMAP: Tracks G-J + two near-term quick wins (editable chmod dialog, CLI conflict-policy flags). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The real sharing use case (remote employee, link-based, over the internet) needs auth regardless of delivery, so the whole track is parked rather than shipping the LAN-only half. Scoped-grant model stays the backbone for when login lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Safety hardening for the continuous folder-sync engine (Plan 2, Track A). Exclude patterns: each pair gains a gitignore-style `exclude` list. Matching files are filtered out of BOTH copies and deletes in the reconciler, so an excluded path is never uploaded and — critically — never mirror-deleted on the far side. Basename patterns (`node_modules`, `*.log`) match at any depth; path patterns (`/dist`, `build/**`) anchor at the root and cover descendants. Mirror-delete guard: gates the destructive half of a Mirror sync while letting uploads through. - Empty/unreadable source root (unmounted drive, deleted folder, typo) no longer wipes the destination — the diff's "everything is gone" is refused. - A configurable per-reconcile delete cap (`mirrorDeleteCap`, default 100) bounds the blast radius; over-cap deletes are withheld with a warning surfaced on the pair. The exclude + guard decision is factored into `apply_safety`, exercised by an on-disk test that runs the real `sync::plan` over temp dirs and asserts the filtering, the cap, and the empty-source refusal against actual files. Frontend: New-sync-pair form gains an exclude textarea and (under Mirror) a max-deletes-per-sync input; the pair row shows an exclude count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… Plan 3 Cut the persistent state index out of Plan 2 (folder sync) into a new Plan 3 — a shared scan engine + faro.db foundation that disk-usage, diff, and search all build on, rather than sync-private plumbing. Renumber every plan file to execution/build order and fix all cross-references (titles, inline Plan-N mentions, .md links): 3 = scan+index foundation 7 = fleet search 4 = disk usage 8 = fleet skills 5 = additional backends 9 = on-demand virtual folders 6 = directory diff 10 = icons, 11 = sharing (deferred) Add a "Plan build order" index to the ROADMAP and reorder its recommended sequence to match. Plan 2 is now marked code-complete (Phases 1-2 + safety); its remaining work is runtime verification, not code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduces `CLAUDE.md` as a repo working agreement covering Faro architecture, branch/release workflow, autonomy expectations, build/verification commands, and codebase layout. Updates `.gitignore` to exclude `.pr/` so local PR draft files from the `create-pr` skill are kept out of commits.
Add rusqlite (bundled) and a db.rs that opens one faro.db under the app data dir with forward-only, user_version-versioned migrations. First table is sync_state — folder-sync's per-file memory, keyed by (pair_id, rel_path) with the source-side size/mtime plus an opaque change token. No behavior change yet; later plans add tables as additive migrations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New scan.rs owns the recursive RemoteFs walk that sync.rs::plan used to do inline. It lists directories with bounded concurrency (latency, not CPU, dominates on SFTP/FTP) and exposes a progress callback + cooperative cancel token for the visible consumers to come (disk usage, diff, search). sync::plan now calls scan::walk_tree — behavior-preserving; the on-disk planner test still passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ChangeSignal (MtimeSize/Etag/Hash) to Capabilities and an optional etag to DirEntry, so callers stop inferring "did this change" from mtime alone. Every backend declares honestly: object stores report Etag and populate the token from object_store's e_tag; SFTP/FTP/local/agent report MtimeSize. The scan engine now carries the etag through to ScanEntry. Mirrored in the file-ui DirEntry/Capabilities types as optional fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reconciler now loads the pair's sync_state, plans via sync::plan_indexed (which consults the index for same-size edits the live size/mtime diff misses, flagging them SyncReason::Edited), and snapshots the source tree back into the index after each successful pass — seeding it on first run and pruning removed files. This closes the same-size-edit gap and makes relaunch a no-op instead of a full re-upload. Removing a pair clears its index rows. Backends declare their change signal (ETag for object stores, mtime+size otherwise); the index compares the opaque token when present. Added the "edited" reason to the frontend SyncReason union + badge. Verified: a real-Db + real-files test shows the stateless diff missing a same-size edit under a newer dest mtime, the index catching it, and a seeded index declining to re-upload; a file-based Db::open test covers the startup path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A ScanManager (shaped like TransferManager: Arc in AppState, scans in a Mutex<HashMap>, progress via diskscan:// events, cancel via a shared CancelToken) recursively sizes a directory over any RemoteFs by reusing the shared scan engine (scan::walk) and folds the flat file list into an aggregated size tree (DuNode: name/path/kind/size/children, sorted largest-first). Phase 1 is the generic walk that works on every backend; the ScanStrategy enum is wired for the exec/object fast paths in Phase 3. Commands: diskscan_start/status/tree/cancel/forget, registered in lib.rs. scan::ScanProgress gains bytes_found (additive; the sync poller ignores it) so the explorer can show a live byte total. Unit-tested tree aggregation + largest-first sort + root-path handling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A full-screen Disk Usage overlay (DiskUsageHost, driven by a dedicated diskScanStore that owns the scan lifecycle: progress events, cancel, drill-down, colour mode). The centrepiece is a Canvas squarified treemap (DiskTreemap) — recursive binary-split layout, theme-aware fills read from the live CSS tokens, hover readout + click-to-drill — beside a size-ranked "biggest items" list with percentage-of-parent bars. Breadcrumb navigates the drilled path; Rescan/Cancel and a by-type/by-depth colour toggle live in the header. Entry point follows the openTerminal pattern: an optional analyzeDiskUsage hook on the file-ui FileSystemAdapter, implemented in the Tauri adapter to open the explorer, surfaced as a FilePane toolbar button (current dir) + a directory context-menu item. materialIconUrl is now exported from @faro/file-ui so the list matches the browser's file icons. Typed ipc wrappers + onDiskScanEvent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ting
The scan now picks the fastest strategy from the live Session, falling back to
the generic walk on any failure so a scan never hard-fails:
- Object stores (S3/Azure): one flat `store.list(prefix)` returns every key +
size in a single streamed pass — no recursion. Progress ticks as keys arrive.
- SSH: `find <root> -type f -printf '%s\t%p\n'` over the exec channel, parsed
into the same flat file tree the walk produces — orders of magnitude fewer
round-trips than an SFTP walk. A non-zero exit with output (unreadable subdir)
is still accepted; a truly empty failure falls back.
- Faro Agent: the same `find`, via the daemon's bounded Exec (gated by
allowExec); denial/timeout/truncation fall back to the walk.
The chosen strategy shows as the header badge; a fallback records a short note
("exec disabled — used walk") surfaced next to it. diskscan_start now resolves
the Session and threads it through ScanManager::start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Right-clicking a treemap cell or a ranked-list row opens a context menu: - Reveal / Open in file browser — a one-shot revealTarget signal on layoutStore that FileBrowser + DualPaneBrowser consume to point the active pane at the item's folder (closing the explorer). - Copy path — to the clipboard. - Delete — confirmed via the shared ConfirmModal, then pruned from the tree in place (subtracting the freed bytes from every ancestor) so the map + list update instantly without a rescan. The scan root can't be deleted from here. Colour-by-type/depth and Rescan already shipped in Phase 2. "Remember last scan per connection" is intentionally deferred — the entry point always carries the current directory, so its marginal value doesn't justify a faro.db table yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A tokio integration test walks an actual temp directory over LocalFs and folds it into the aggregated size tree (the generic strategy every backend falls back to), plus a parser test that `find`'s output produces the identical tree shape — so the SSH/agent fast path and the walk agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Azure Blob and the aws/r2/b2 presets already shipped, so the plan was stale. Reordered by current cost (native GCS is now a one-builder move via object_store's gcp feature), added new backends (Box, pCloud, NFS stretch, read-only HTTP source, WebDAV provider presets, more S3 presets), resequenced the OAuth tier by API friction (Dropbox first — path-based), and synced ROADMAP Track B. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phases 1–4 land: the scan engine + generic walk, the Canvas treemap explorer + size list, the exec-find / object-flat fast paths, and the reveal/copy/delete actions. Core scan behaviour is runtime-verified against a real filesystem and the app boots clean; a GUI click-through on a live backend is the remaining manual check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Wasabi, DigitalOcean Spaces, MinIO, Storj, Hetzner, Scaleway, Oracle OCI, IBM COS, Supabase, and a generic self-hosted S3 preset to S3_PROVIDER_PRESETS, each with an endpoint template + default region. guessProvider now recognizes their endpoint patterns so an imported/edited profile preselects the right button, and the provider grid renders every preset from the table (vendor sub-label per row) instead of a hardcoded three. No backend code: the object-store connect path already treats any custom endpoint as path-style S3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GCS is the same one-builder move that shipped Azure: enable object_store's gcp feature and add gcs_connect() using GoogleCloudStorageBuilder. The bucket rides in profile.bucket like an S3 bucket / Azure container; the service-account key comes from either a JSON file path (Key auth) or the pasted JSON (Password auth). A GCS session is an ObjectSession, so browse, transfer, sync, disk-usage, and rename/delete all work through the existing ObjectFs + object transfer arms unchanged. Frontend: add the gcs Protocol (default port 443, label GCS, object-store group), a GcsSection in the New Connection editor with a file/paste key toggle, and route gcs through every protocol switch (rail badge gs://, bridge/import/search icons, mock capabilities). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new RemoteFs impl over WebDAV (Nextcloud, ownCloud, Hetzner Storage Box, and the generic long tail). One protocol, real directories, rename via MOVE, no chmod; change detection uses the per-resource ETag with mtime+size as the fallback for servers that omit getetag. Backend: - session/webdav.rs — WebdavSession (shared reqwest client, base URL carrying the DAV root, Basic/Bearer auth). Connect validates creds + URL with a depth-0 PROPFIND. url_for percent-encodes each path segment. - remotefs/webdav.rs — WebdavFs: list_dir (PROPFIND depth 1), rename (MOVE, no-clobber), delete (DELETE), create_dir (MKCOL), capabilities. A namespace- prefix-agnostic multistatus parser + a dependency-free RFC1123 date parser. - transfer.rs — streaming GET download (real progress) + streaming PUT upload; HEAD-based size/overwrite probes. - Session::Webdav variant threaded through session connect/disconnect, fs_for_session (commands + transfer), and editor.rs edit-in-place. Frontend: webdav Protocol (label WebDAV, HTTP :443, Globe icon), a WebdavSection in the New Connection editor with provider presets (Nextcloud/ownCloud/Storage Box/generic) that prefill the DAV URL template and a Basic/Bearer auth toggle; rail address shows the server URL. Verification: 5 parser unit tests (Nextcloud, Apache mod_dav, full-URL hrefs, HTTP-date, etag cleaning) + a live end-to-end #[ignore] test exercising connect -> MKCOL -> PUT -> PROPFIND -> GET -> MOVE -> DELETE through Faro's own WebdavFs, run green against a local wsgidav server with Basic auth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phases 0 (S3 presets), 1 (native GCS), and 2 (WebDAV) are built and verified. Note the Phase 3 SMB build-time finding: pavao/libsmbclient has no MSVC build and the pure-Rust smb crate is immature, so SMB is deferred pending a viable client + a NAS to verify against. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Browse any static file server as a Faro connection. A new RemoteFs impl that lists via autoindex and reads via GET; every mutation errors (read-only). Backend: - session/http.rs — HttpSession (shared reqwest client, optional Basic auth). Connect decides Listing vs DirectFile mode from the URL shape (trailing slash / a filename extension) so pasting a direct artifact URL works with no listing, and probes reachability + auth with a HEAD. - remotefs/http.rs — HttpFs: list_dir parses nginx/Apache autoindex HTML (anchor-based, handles <pre> and <table>) or nginx JSON autoindex; DirectFile mode HEADs the single file. rename/delete/create_dir/chmod all return a read-only error. change_signal Etag (from the file's own ETag/Last-Modified). - transfer.rs — streaming GET download; upload errors; HEAD-based size. - Session::Http threaded through connect/disconnect, both fs_for_session copies, and editor.rs (download works, save errors). - Shared webdav::parse_http_date (now pub(crate)) for Last-Modified / JSON mtime. Frontend: http Protocol (label HTTP, read-only :443, Download icon), an HttpSection with a URL field + optional Basic-auth toggle; rail address shows the URL. Verification: 5 parser unit tests (nginx HTML, Apache table with human sizes, nginx JSON, size tokens) + a live #[ignore] end-to-end test run green against python -m http.server in both listing and direct-file modes — list, GET round-trip, and refused mutations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oauth.rs: a provider-agnostic OAuth 2.0 loopback + PKCE (S256) authorization- code helper with no external OAuth crate — a fixed-port (:53682) local listener catches the redirect, the code is exchanged against a configurable token endpoint, and refresh is a second form POST. Endpoints are OAuthConfig fields so tests can point them at a local mock. Tokens persist in the OS keychain via the keyring crate (native backends only — no dbus dependency), never in profile JSON. Verified: PKCE S256 matches the RFC 7636 reference vector; authorize-URL, token-parsing, and expiry unit tests pass; a keychain write->read->delete round-trip runs green against Windows Credential Manager. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dropbox API v2 is path-addressed, so it slots straight into the trait. - session/dropbox.rs — DropboxSession: bearer-token RPC/content helpers with transparent token refresh (proactive on near-expiry + retry-once on 401), re-persisting refreshed tokens to the keychain. Endpoints fall back to env overrides so the flow can be pointed at a mock. account_label/exists/size helpers. Tokens loaded from the keychain at connect (errors clearly if the connection was never authorized). - remotefs/dropbox.rs — DropboxFs: list_folder (+ /continue pagination), move_v2, delete_v2, create_folder_v2; rev as the change token; ISO-8601 server_modified parsing. Faro↔Dropbox path mapping (/ ⇒ ""). - transfer.rs — streaming download (/2/files/download) + simple upload (/2/files/upload, overwrite; >150 MB refused with a clear message pending chunked upload_session); size/overwrite probes via get_metadata. - Session::Dropbox threaded through connect/disconnect, both fs_for_session copies, and editor.rs edit-in-place. Verified by unit tests (path mapping, ISO-8601, list_folder parsing incl. deleted-tombstone drop). OAuth infra reused from Phase 5a. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dropbox_authorize command runs the interactive OAuth flow (browser + loopback redirect), stores tokens in the OS keychain keyed by profile id, and returns the account label — mirroring agent pairing, so a cancelled flow leaves no saved connection. delete_profile now clears a Dropbox profile's keychain tokens. Frontend: dropbox Protocol (label Dropbox, Box icon, OAuth hint), a DropboxSection with a 'Connect with Dropbox' button that authorizes then gates Save on success and shows 'Connected as <account>'; rail address shows the account. Typed ipc.dropboxAuthorize wrapper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rust commands returned Result<T, String>, so the frontend had only the
Display text and had to regex English to tell an auth failure from a missing
file. Errors now serialize as {kind, message} with a stable discriminant.
- New `error` module: FaroError + ErrorKind (auth/permission/not-found/
network/timeout/unsupported/conflict/other), classifying an anyhow error by
io::ErrorKind downcast then message keywords — once, in Rust, so the frontend
never repeats the matching.
- Migrated the remotefs file-op group (rename/delete/create_dir/chmod) and
`connect` to Result<_, FaroError>. Un-migrated commands keep returning String
and render as generic toasts — the rollout is incremental by construction.
- Frontend `src/lib/errors.ts`: kindOf / messageOf / isFaroError + `toastError`,
which maps each kind to an actionable toast (auth → "reconnect", network →
"check the connection"), falling back to a generic toast for string errors.
- Wired connectionsStore + diskScanStore to `toastError`; taught the file-ui
package's error banner to read `.message` off structured errors instead of
stringifying them to "[object Object]".
Verified: error-module unit tests (serialization + classification) pass; a new
headless harness (scripts/verify-errors.mjs) drives connect with structured
errors and asserts the rendered toast is keyed off the kind, with the legacy
string path still working. cargo check + tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Faro holds real value on a machine — 13 backends' worth of profiles, their keychain secrets, and faro.db state — but moving to a new machine meant re-entering all of it. Now one password-protected file carries everything. - New `backup` module: container = magic `FAROBAK\x01` + version + Argon2id salt + AES-GCM nonce, then AES-256-GCM over gzip(archive). The header is the AEAD's associated data, so a wrong password fails the tag cleanly and nothing partially decrypts. Argon2id at 64 MiB / t=3. - Contents: profiles.json, a WAL-safe faro.db snapshot (VACUUM INTO), bridge + foldersync configs, and every keychain credential — the Anthropic key via the faro.db manifest plus each cloud profile's OAuth tokens (enumerated since the OS keychain can't be listed). - Restore stages files as `<name>.restore` and injects credentials into the keychain immediately (never written to disk in the clear); a startup hook swaps the staged files in atomically before anything opens them. - Commands `backup_export` / `backup_inspect` / `backup_import`; a Settings → Backup danger zone with a "what's inside" preview before restoring; and `faro-cli backup export|import` (+ a FARO_DATA_DIR override) for headless migration. Verified at runtime: round-trip + wrong-password unit tests pass; the real CLI binary exports a valid FAROBAK container (2 synthetic and 10 real profiles), imports it into a fresh data dir with profiles.json matching byte-for-byte, and rejects a wrong password. cargo check (GUI + CLI) + tsc clean; 104 lib tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All four phases built and runtime-verified — keychain-everything credentials,
settings in faro.db with pre-paint injection, structured {kind, message} IPC
errors, and encrypted backup/restore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 13 Phase 1 backend. Adds a `preview.rs` PreviewManager and a `preview_thumbnail` command that turns a remote (or local) image into a small cached PNG thumbnail without ever risking an unbounded request: - `read_head` — a bounded per-Session byte read (mirrors transfer::remote_size's dispatch) covering local/SFTP/object/FTP/WebDAV/HTTP/agent; caps the transfer at 25 MiB and skips larger files. - Decode + downscale to a 160px PNG in Rust (`image` crate, off a blocking task). - Per-connection concurrency cap (Semaphore, 4) so a screenful scrolling in at once can't swamp the session pool. - LRU disk cache: a `thumb_cache` table (faro.db v5) keyed by (session, path, dim, change-signal) so edits invalidate; evicts down to a 256 MiB budget after each write. Unit-tested: decode/downscale bounds, cache-key invalidation, a local round-trip through the cache, and the DB LRU eviction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y budget) Plan 13 Phase 1 frontend. Wires the new `preview_thumbnail` backend into the file-ui thumbnail pipeline, gated so scrolling never fires unbounded requests: - Setting `remoteImagePreviews` (default off) in the settings store + a Settings "File panes" toggle, plus a per-session toggle in the file-pane toolbar (shown for remote sessions only). Flipping it re-keys the thumbnail loader so mounted rows re-fetch immediately instead of waiting for a navigation. - Adapter `thumbnail` now handles remote images: opt-in check, raster-only + size guard, an AbortSignal threaded from the row's IntersectionObserver (a row that scrolls out cancels before dispatch), and a per-connection concurrency limiter (4). Local previews unchanged. - file-ui: `thumbnail(…, signal?)` contract, Thumbnail aborts on unmount, and list/details rows now show thumbnails too (not just grid) when enabled. Runtime-verified headlessly (scripts/verify-previews.mjs): off → icons only, toolbar toggle → thumbnails for the on-screen image rows in grid and list, non-images stay icons, toggling back → icons. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 13 Phase 3 foundation. Some hosts disable the SFTP subsystem but still run an exec shell (busybox appliances, locked-down boxes); those can't be reached over russh-sftp but can transfer bytes over scp's old rcp-derived wire protocol. - `scp.rs` — the SCP protocol (`C`/`T` records + status-byte acks) over a generic `AsyncRead + AsyncWrite`: `download_to` (server runs `scp -f`), `upload_from` (server runs `scp -t`), single-quoted path safety, and server error/warning messages surfaced verbatim (not masked by a wrapper context). Unit-tested over `tokio::io::duplex` with an in-process server side — the real byte exchange round-trips a file each way, skips a leading mtime record, and propagates a "No such file" / "Permission denied" from the server. - `SshSession::scp_download_to` / `scp_upload_from` — run the protocol over an exec channel's stream, plus `sftp_available()` to detect when to fall back. Remaining Phase 3 (its own slice, best verified against the Plan 13 Phase 5 busybox fixture): an ScpFs that browses via the shell + a New-Connection "SCP mode" toggle + routing SSH transfers through these methods when SFTP is off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…landed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a recognizable brand/protocol logo layer via Iconify, bundled fully offline. This is additive — lucide still owns UI chrome, Material Icon Theme still owns file-type icons; Iconify covers the one gap neither does (actual AWS S3 / Azure / SSH / lighthouse marks on connections). - scripts/gen-brand-icons.mjs extracts only the ~13 icons Faro references from the @iconify-json/* sets into a committed src/lib/brandIconData.ts (9.7 KB), so no whole 2,000-icon set is bundled. - src/lib/brandIcons.tsx: protocolIcon(protocol) map + a thin <BrandIcon> over @iconify/react/offline's <Icon>, fed raw icon data (never a network name). The offline entry carries zero api.iconify.design references. - @iconify/react is a runtime dep; the JSON sets + @iconify/utils are devDeps used only by the generator. - Licences recorded in THIRD_PARTY_LICENSES.md (logos & simple-icons CC0, mdi Apache-2.0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire protocolIcon()/<BrandIcon> into the three connection surfaces: - ServerRail bubbles gain a small top-left brand chip (S3 / Azure / SSH / the Faro lighthouse) while the colour monogram stays the primary identity — logo PLUS colour, never instead of. - The connection-list search popover rows swap the generic cloud/server lucide glyph for the real protocol logo. - The New-Connection protocol picker shows each protocol's real mark; colour brands (S3/Azure/GCS/Dropbox/OneDrive/Drive) render in their own palette, monochrome glyphs (SSH/FTP/WebDAV/HTTP/Box/lighthouse) follow currentColor. BrandIcon tags its <svg> with a `brand-icon` class for styling/testing and letterboxes non-square marks into a square slot (no distortion). Removed the now-dead lucide-based icon pickers and unused imports. Verified headlessly via scripts/verify-brand-icons.mjs (mock harness): brand logos render on the rail/list/picker, the monogram survives, colour vs mono render correctly, and the page makes ZERO requests to iconify.design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reuse <BrandIcon> for the "known service type" surface — the New-Connection S3 and WebDAV provider preset buttons. The generic lucide Cloud/Globe glyphs become real vendor marks: Cloudflare R2, Backblaze B2, DigitalOcean Spaces, Wasabi, MinIO, Hetzner, Scaleway, Oracle OCI, IBM COS, Supabase for S3, and Nextcloud / ownCloud / Hetzner for WebDAV. Providers with no bundled mark (Storj, the self-hosted "generic" presets) keep the neutral lucide fallback. Curated set grows to 25 icons (29 KB data, ~+9 KB gzip to the bundle); all still CC0 (logos/simple-icons). Attribution updated. Harness extended to assert vendor logos render in both provider grids and R2 shows in colour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Iconify brand/protocol logos landed: offline foundation, protocol logos on the rail/connection-list/picker (logo + monogram), and vendor logos on the S3/WebDAV provider presets. Phase 4 (lucide consolidation) stays deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 15 Phase 1 substrate. Shortcut overrides ride Plan 12's settings table as `shortcut.<id>` rows, so "reset to default" means removing the row — the one thing the existing settings commands couldn't do. Add db.settings_delete + the settings_delete Tauri command (registered in lib.rs), the ipc wrapper, and mock handling. No other backend changes: the pre-paint injector already emits every settings row onto window.__FARO_SETTINGS__, so overrides seed synchronously, and the encrypted backup carries them for free. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn the hardcoded command combos into data (Plan 15 Phase 1). New bindingsStore holds the user's overrides, keyed by command/action id and seeded synchronously from the pre-paint settings injection (shortcut.* rows), persisted via settingsSet/settingsDelete. keybindings.ts adds effectiveCombo (override ?? default) and useResolvedCommands — the single resolver every surface reads so the palette, menus, cheat-sheet and dispatcher can't drift onto private copies of a combo. The file-browser keys are handled locally inside <FilePane> (they need the pane's live selection) but remap through the same layer: the @faro/file-ui package now exports a FileBrowserAction catalog + default combos + a self-contained combo matcher, and PaneSettings gains an optional keyBindings map. fileBrowserKeys.ts is the app-side catalog + effective-combo resolver that threads them in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…appable chords Wire every shortcut surface to the effective-combo resolver (Plan 15 Phases 1, 3, 4). useShortcuts drops the blanket ctrl/meta gate: it now dispatches bare-key combos too, guarded so they never fire while focus is in an input/textarea/ terminal (modifier combos stay global). The palette, cheat-sheet and title-bar menus read useResolvedCommands, so a remap shows up consistently; the cheat-sheet also lists the new file-browser keys from the catalog. Terminal split/zoom/close chords become remappable: isTerminalChord (xterm's swallow-matcher) reads the effective bindings from the store, but only ever swallows a chord that still holds a modifier so a bad remap can't eat shell keys. The mock harness exposes useBindings for headless verification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 15 Phase 3. FilePane's keydown handler now resolves its actions from an effective combo map (host overrides over built-in defaults) and gains the keys every file manager has: F2 rename, Space quick-info (properties), Ctrl/Cmd+Shift+N new folder, and F5 refresh — alongside the existing Enter open, Delete, Backspace go-up, Ctrl/Cmd+A select-all, Escape unwind and type-ahead. All are remappable and respect the typing guard (nothing fires while editing text). FileUiBridge threads the effective bindings in from the shared layer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 15 Phase 2. A searchable, grouped list of every remappable binding (the app commands that carry a default combo plus the file-browser action catalog), each with a click-to-record capture field (Esc cancels, Backspace resets to default), conflict detection that offers to reassign + unbind the previous holder, a per-row reset, and reset-all. Recording captures the next keystroke via a capture-phase window listener so it can't leak into the global dispatcher or the dialog's Escape-to-close. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/verify-shortcuts.mjs drives the mock build in a headless browser and asserts the real behaviour: a remapped combo fires while the stale default no longer does, the cheat-sheet reflects the effective combo + new file-browser keys, F2 opens a rename in the browser but is inert while typing in the filter, the Settings Keyboard tab surfaces a conflict for a taken combo, and reset-all clears overrides. All 12 checks pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces three new planning docs for keyboard shortcuts/remapping, app updater + notifications + PATH integration, and transfer queue depth. The old scoped connection sharing plan is renumbered to Plan 99, marked out-of-scope, and removed from active build sequencing. ROADMAP.md is updated to reflect these additions, revised plan statuses (including Iconify progress), and new Track O/P/Q sections.
Plan 16 Phase 4. A new path_integration.rs behind path_status / path_add /
path_remove, mirroring how dev tools like `code` and `gh` install their shell
command — at the per-user level so it never needs admin/UAC.
- Windows: append the app-owned bin/ dir to HKCU\Environment\Path via winreg,
reading/writing the exact existing value with its registry type preserved
(REG_EXPAND_SZ stays REG_EXPAND_SZ, %VARS% unexpanded), deduped and
idempotent, then broadcast WM_SETTINGCHANGE("Environment") so new terminals
pick it up. setx is deliberately never used (1024-char truncation + type
flattening). The prior value is snapshotted into faro.db (v6 env_backup table)
before the first write.
- macOS/Linux: symlink faro-cli into ~/.local/bin and, if that dir isn't on
PATH, append one marker-guarded export line to the detected shell profile so
removal is exact.
- Removal only ever touches Faro's own entry/symlink/marker line.
Verified: unit tests for the dedupe/marker/backup logic; a real round-trip
against the live HKCU\Environment\Path (REG_SZ here) adds exactly one entry, no
duplicate on re-add, and removes to a byte-identical value with the type
preserved (win_tests, including an ignored real-Environment end-to-end).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 16 Phase 4 frontend. A new About settings section (app version + the PATH integration row) that will also host the updater card and notification toggles as those phases land. The PATH row reads path_status and drives Add ⇄ Remove, keying the primary action off the `managed` flag (our entry is present right after add even though the running process's own PATH won't reflect it until a new terminal). Typed ipc wrappers + PathStatus type; mock handlers + a src/mock/app.ts (aliased in mock mode) so the flow round-trips headlessly. Verified end-to-end in a headless browser (scripts/verify-path-integration.mjs): About tab renders the row in the not-on-PATH state, Add flips it to managed with a Remove button, Remove returns it — all green, clean visual. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 16 Phase 3 plumbing. Adds tauri-plugin-notification (Rust) + @tauri-apps/plugin-notification (JS) and the notification:default capability, so the webview can raise OS toasts. The events themselves are decided/gated frontend-side (next commit) — this just exposes the API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 16 Phase 3. A frontend notifications layer (lib/notifications.ts) that reuses the existing event stream — it's decided here, not in Rust, because the frontend already tracks all this state and can cheaply check window focus: - a transfer batch draining the active queue → one summary toast (done/failed counts), never one-per-file; - a folder-sync pair transitioning *into* its error state (not while it stays there); - an edit-in-place save failing. All behind a new `notifications` setting (default on, unfocused-only) surfaced as two toggles in Settings → About; permission is requested lazily on the first eligible event; clicking a toast focuses the window and opens the relevant panel. Fixes a StrictMode double-subscribe race (async subscribe vs sync cleanup) that would otherwise fire every toast twice in the main window. Verified headlessly (scripts/verify-notifications.mjs, 8 checks): unfocused → one toast, focused → none, toggled-off → none, correct done/failed vs all-success messages, editor-save-failure names the file, sync-error fires once and doesn't repeat while it stays in error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 16 Phase 1. Registers tauri-plugin-updater + tauri-plugin-process (Rust) and
their JS plugins, with the updater pointed at a signed GitHub-Releases
latest.json:
- tauri.conf.json: plugins.updater { endpoints, pubkey, windows.installMode:
passive }. The pubkey is the public half of a freshly-generated minisign
keypair; the private half is gitignored (.updater/) and documented for CI
custody — signature verification is the whole security model.
- capabilities: updater:default + process:allow-restart.
- updater-release.conf.json: a config overlay that flips on
createUpdaterArtifacts, applied by CI only when the signing secret is present
(see the release-workflow commit) so releases never break before the key is
wired.
Verified: full cargo build links with all three plugins; generate_context!
validates the updater config at compile time; the real app boots clean and the
updater performs a live check against the configured GitHub endpoint (logs a
graceful "no successful status" since no signed latest.json is published yet) —
no crash, pubkey parses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 16 Phase 2. Drives the updater plugin from a small Zustand store (updaterStore) rather than bespoke Rust commands (the Tauri v2 idiom): - a throttled quiet check on launch (persisted via lastUpdateCheck in faro.db, 6h interval) that swallows the expected "no latest.json yet" error; - Settings → About "Updates" card: Check for updates → up-to-date / available (version + notes link) → Download & install (progress) → Restart now; - a dismissible UpdatePrompt bar above the status bar mirroring CliUpdatePrompt, shown when an update is available/downloading/ready. Restart uses the process plugin's relaunch(). Mock plugins + aliases let the flow run headlessly. Verified end-to-end (scripts/verify-updater.mjs): up-to-date → 'latest version'; available → shows the offered version + Download; download → progress → 'ready' with the mock artifact installed; Restart now calls relaunch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan 16 Phase 1 CI. The gui job now passes TAURI_SIGNING_PRIVATE_KEY/_PASSWORD from secrets and, only when the key is set, applies the updater-release.conf.json overlay (createUpdaterArtifacts) so tauri-action produces signed artifacts + uploads latest.json. Without the secret it builds exactly as before and prints a warning — adding the updater can't break the existing release pipeline. docs/updater-key-custody.md documents the keypair, the two secrets to add, and a recommended pre-release key rotation (the key was generated in a dev session). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR delivers a large “trust + daily usability” upgrade across Faro’s Tauri backend (Rust) and React/Zustand frontend: moving secrets into the OS keychain, migrating settings into faro.db with pre-paint injection, adding structured IPC errors, and layering in major UX features (terminal splits/snippets, remote thumbnails, remappable shortcuts, brand icons, updater/notifications/PATH integration).
Changes:
- Security & portability hardening: OS keychain secrets,
faro.db-backed settings w/ pre-paint injection, structured{kind, message}IPC errors, and encrypted backup/restore (plus CLI support). - Terminal + productivity depth: terminal split/zoom/close chords, snippets with
{{variables}}insertion, and a focused-terminal injection registry. - UX/platform features: opt-in remote image thumbnails (lazy + gated), fully remappable keyboard shortcuts (including file browser), offline brand/protocol logos, and signed in-app updater + desktop notifications + one-click PATH integration (with mock/test harness scripts).
Reviewed changes
Copilot reviewed 94 out of 97 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Adds Vite aliases for mock Tauri APIs/plugins used by headless/demo builds. |
| THIRD_PARTY_LICENSES.md | Adds Iconify-related licensing and Apache 2.0 notice for MDI usage. |
| src/stores/updaterStore.ts | Zustand store driving update check/download/install/relaunch with throttled launch checks. |
| src/stores/snippetsStore.ts | Zustand store for snippet CRUD + insert flow (variables/multiline confirm). |
| src/stores/diskScanStore.ts | Uses structured error toast helper for delete failures. |
| src/stores/connectionsStore.ts | Uses structured {kind,message} handling for connection errors + actionable toasts. |
| src/stores/bindingsStore.ts | Stores per-command shortcut overrides (shortcut.<id>) seeded from pre-paint settings injection. |
| src/stores/agentChatStore.ts | Removes API key from frontend; checks key presence only, backend reads from keychain. |
| src/mock/window.ts | Adds mock focus APIs (isFocused, setFocus) for notification gating/tests. |
| src/mock/updater.ts | Mock plugin-updater implementation for headless verification. |
| src/mock/process.ts | Mock plugin-process relaunch/exit for headless verification. |
| src/mock/notification.ts | Mock plugin-notification capturing sent notifications for assertions. |
| src/mock/demo.ts | Exposes additional stores on window.__demo for verification scripts. |
| src/mock/data.ts | Adds an image-heavy uploads folder fixture for preview verification. |
| src/mock/core.ts | Expands mock IPC surface (settings/keychain, snippets, preview thumbnail, PATH, structured errors, multi-pane terminals). |
| src/mock/app.ts | Mock @tauri-apps/api/app to provide app/version metadata in demo build. |
| src/main.tsx | Makes theme set-from-store a fallback only (main window now pre-themed by Rust injection). |
| src/lib/types.ts | Adds types for SSH key generation, PATH integration, snippets, and backup summaries. |
| src/lib/termSuggest.ts | Allows terminal to swallow app-level chords so they don’t reach the shell. |
| src/lib/termInput.ts | Adds focused-terminal pane registry for safe snippet/tool injection at prompt (no auto-run). |
| src/lib/terminalChords.ts | Centralizes terminal split/zoom/close chord defaults + xterm swallow matcher honoring overrides. |
| src/lib/snippets.ts | Implements {{variable}} extraction/substitution and trailing-newline stripping. |
| src/lib/secretMigration.ts | One-time migration: localStorage secret → keychain; localStorage settings → faro.db with verify-then-delete. |
| src/lib/notifications.ts | Frontend-owned desktop notification wiring (focus-gated, setting-gated, curated events). |
| src/lib/keybindings.ts | Resolves effective shortcuts (override ?? default) and exposes resolved command registry. |
| src/lib/ipc.ts | Adds typed IPC wrappers for SSH keys, previews, snippets, settings, keychain status/set, backup, PATH integration. |
| src/lib/fileUiAdapter.ts | Adds remote thumbnail pipeline with per-session concurrency gating + opt-in setting; updates adapter thumbnail signature. |
| src/lib/fileBrowserKeys.ts | Declares file-browser action bindings and resolves overrides for @faro/file-ui. |
| src/lib/errors.ts | Adds frontend structured error helpers (kindOf, messageOf, toastError). |
| src/lib/commands.tsx | Adds snippets command entry, terminal split/zoom/close commands, and per-snippet insert commands. |
| src/lib/brandIcons.tsx | Offline Iconify wrapper + protocol→icon mapping for recognizable provider/protocol marks. |
| src/hooks/useShortcuts.ts | Switches dispatcher to resolved commands; supports bare keys with editable-target guards. |
| src/components/VariableDialog.tsx | Snippet insert dialog for variables + multiline preview/confirm. |
| src/components/UpdatePrompt.tsx | Non-blocking in-app update prompt bar (download/progress/restart). |
| src/components/TitleBar.tsx | Menu shortcut labels now reflect effective (override-aware) bindings. |
| src/components/ServerRail.tsx | Replaces generic protocol glyphs with offline brand/protocol icons in rail/search UI. |
| src/components/KeyboardShortcutsDialog.tsx | Displays resolved command shortcuts plus file-browser bindings (override-aware). |
| src/components/FileUiBridge.tsx | Threads file-browser keybindings + remote preview toggle into @faro/file-ui pane settings. |
| src/components/CommandPalette.tsx | Uses resolved commands so palette reflects remapped shortcuts. |
| src/App.tsx | Hooks up migrations, notifications init, snippets host, and update prompt. |
| src-tauri/updater-release.conf.json | Config overlay enabling updater artifact generation when signing is enabled. |
| src-tauri/tauri.conf.json | Moves window creation to Rust setup; configures updater endpoints/pubkey and install mode. |
| src-tauri/src/session/mod.rs | Adds SFTP availability probe and SCP transfer helpers; expands tilde for SSH key paths. |
| src-tauri/src/lib.rs | Registers new plugins; adds pre-paint settings init script; sets up preview manager; registers new commands. |
| src-tauri/src/error.rs | Introduces structured FaroError { kind, message } with classification logic + tests. |
| src-tauri/src/credentials.rs | Implements OS keychain service credential store with “no secret over IPC” rule. |
| src-tauri/src/agent.rs | Reads Anthropic API key from keychain at call time; removes API key from IPC request type. |
| src-tauri/faro-cli/src/main.rs | Adds encrypted backup export/import CLI commands + FARO_DATA_DIR override support. |
| src-tauri/Cargo.toml | Adds deps for notifications/updater/process, thumbnails (image), backup crypto (argon2, aes-gcm), PATH integration (windows-sys), SSH key writing (ssh-key). |
| src-tauri/capabilities/default.json | Grants permissions for notification/updater/process plugins. |
| scripts/verify-updater.mjs | Headless verification script for updater UX flow in mock build. |
| scripts/verify-terminal.mjs | Headless verification script for terminal splits/zoom persistence and snippets UI. |
| scripts/verify-previews.mjs | Headless verification script for remote thumbnail toggle and rendering behavior. |
| scripts/verify-path-integration.mjs | Headless verification script for one-click PATH add/remove UX in mock build. |
| scripts/verify-notifications.mjs | Headless verification script for notification gating (focus + settings) and curated events. |
| scripts/verify-errors.mjs | Headless verification script for structured IPC error → actionable toast mapping. |
| scripts/gen-brand-icons.mjs | Generates a curated offline Iconify subset into a committed data file. |
| packages/file-ui/src/lib/keys.ts | Adds file-browser action enum + default combos + combo builder helper. |
| packages/file-ui/src/index.ts | Exports new keybinding utilities/types from the package. |
| packages/file-ui/src/context.tsx | Extends adapter thumbnail API to accept AbortSignal; adds pane settings for remote previews + keybindings. |
| packages/file-ui/src/components/Thumbnail.tsx | Adds AbortController to cancel thumbnail work when rows scroll out/unmount. |
| package.json | Adds Iconify deps + generator script; adds Tauri updater/notification/process plugins. |
| docs/updater-key-custody.md | Documents updater signing key custody and CI secret setup. |
| docs/plans/99_scoped-connection-sharing.md | Marks scoped-connection-sharing plan as out-of-scope/deferred (renumbered). |
| docs/plans/17_transfer-queue-depth.md | Adds new plan doc for future transfer-queue depth improvements. |
| docs/plans/16_app-updater-and-notifications.md | Adds plan doc describing updater/notifications/PATH approach and constraints. |
| docs/plans/15_keyboard-shortcuts.md | Adds plan doc for remappable shortcuts + file-browser keys and dispatcher rules. |
| docs/plans/14_iconify-brand-icons.md | Adds plan doc + status notes for offline Iconify integration and verification. |
| docs/plans/13_remote-previews-and-protocol-depth.md | Adds plan doc for remote previews and protocol depth roadmap. |
| docs/plans/12_security-settings-and-portability.md | Adds plan doc for keychain/settings/errors/backup hardening. |
| docs/plans/11_terminal-depth-and-snippets.md | Adds plan doc for terminal registry/splits/snippets and verification. |
| CLAUDE.md | Updates plan numbering reference. |
| .gitignore | Ignores updater private key directory and marketing scratch dir. |
| .github/workflows/release.yml | Makes updater signing conditional on secrets; adds config overlay only when signing is enabled. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+64
to
+66
| check: async (quiet) => { | ||
| if (get().status === "checking" || get().status === "downloading") return; | ||
| set({ status: "checking", error: null }); |
Comment on lines
+55
to
+58
| onKeyDown={(e) => { | ||
| // Enter submits from a single-line field; Shift+Enter / textareas are | ||
| // untouched. Esc is handled by useDialog. | ||
| if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Faro already spoke to thirteen backends — this is the batch where it grows up. Six plans of depth and trust land together, none of them a new protocol and all of them the sort of thing that makes an app feel safe to hand real credentials and pleasant to live in all day. The centerpiece is security: the Anthropic API key moves out of a plaintext
localStorageblob and into the OS keychain (it never crosses the IPC boundary as a value again), app settings graduate tofaro.dbwith a pre-paint window injection so there's no flash-of-defaults on boot, IPC errors gain a structured{kind, message}shape the UI can actually act on, and a password-protected encrypted backup packs every profile, secret, and bit of state into a single file you can carry to a new machine. Around that core the terminal becomes a first-class surface — split panes backed by an out-of-React xterm registry that survives remounts — remote image thumbnails render lazily without ever turning a scroll into a download storm, keyboard shortcuts become fully remappable with their own Settings tab, connections pick up real brand logos, and a signed in-app updater checks, downloads, and restarts. Two smaller wins ride along: generating SSH keys in-app (goodbye PuTTYgen) and a one-click "Add faro-cli to PATH." The release workflow only signs updater artifacts when the signing key is present, so shipping auto-updates can never break the pipeline for anyone who doesn't hold the key.Highlights
{{variables}}— save parameterized commands and drop them into the terminal from a panel, the command palette, or a terminal toolbar; they're inserted at the prompt, never auto-run.faro-clito your PATH in one click from Settings → About.Technical changes
Security & portability (Plan 12)
credentials.rs— OS-keychain store under the single servicecom.faro.credentials, keyed by purpose. One-wayset_secret/has_secret/delete_secretexposed to the frontend;get_secretis Rust-only and deliberately never a Tauri command.agent.rsfetches the Anthropic key from the keychain at call time. Every write is mirrored into afaro.dbmanifest (db::record_keychain) so backup knows which entries to export.src/lib/secretMigration.ts— one-time migration of the legacy plaintextfaro.settings.v1API key into the keychain, scrubbing the localStorage field only after a successful write (retries next launch on failure).faro.db—settingstable +settings_get_all/settings_set_all/settings_delete. The main window is built insetup()with aninitialization_scriptthat seedswindow.__FARO_SETTINGS__before first paint (no flash-of-defaults).runSettingsMigrationmigrates-then-verifies before removing the localStorage blob.error.rs—FaroError { kind, message }with a kebab-caseErrorKindand a classifier that prefers a concreteio::Errorkind anywhere in the cause chain, then falls back to message keywords.src/lib/errors.tspattern-matcheskindinto actionable toast copy and treats un-migrated string errors asother, so both paths render sensibly.backup.rs— theFAROBAK\x01container: Argon2id key (m=64 MiB, t=3, p=1), AES-256-GCM over gzip'd archive JSON with the full header as AEAD associated data (a wrong password or tampered header fails the GCM tag cleanly). Packs profiles, subsystem configs, a WAL-safefaro.dbsnapshot, and every keychain credential. Restore stages<name>.restorefiles and injects credentials into the keychain;apply_pending_restoreswaps the staged files in at next startup, while the CLI path applies immediately.Terminal depth & snippets (Plan 11)
terminalRegistry.ts— xterm instances and their DOM nodes live outside React, keyed by a stable pane id. Componentsattach/detachthe cached node so scrollback, PTY, and listeners survive remounts, split restructures, popouts, and HMR.detach(container?)guards against a re-parent pulling a node out of a leaf that just adopted it.terminalsStore.ts— a per-tab layout tree (leaf | split); disposal is driven explicitly by the store, which disposes panes beforeset()rather than inside the updater.snippetsStore.ts,SnippetsPanel.tsx,VariableDialog.tsx, andsnippets.ts:{{variable}}extraction/resolution, a multi-line confirm, a use-count bump on insert, and injection at the focused prompt viatermInput.ts(never auto-submitted). Backed by asnippetstable infaro.db.Remote previews & protocol depth (Plan 13)
preview.rs—PreviewManager: bounded remote read (≤ 25 MiB), decode + downscale via theimagecrate on a blocking task to a ≤ 160 px PNG, and an LRU disk cache (256 MiB budget) indexed infaro.dbby (connection, path, change-signal). Per-connection concurrency cap of 4 behind the frontend's viewport gate.fileUiAdapter.ts,Thumbnail.tsx, andFilePane.tsx.scp.rs— the rcp-derived SCP wire protocol against a genericAsyncRead + AsyncWrite(download_toexecsscp -f,upload_fromexecsscp -t), status-byte gated, with single-quoted remote paths (quote_path). Wired intoSshSessiontransfer methods as a fallback for hosts that disable the SFTP subsystem.Keyboard shortcuts (Plan 15)
bindingsStore.tsholds only the user's overrides asshortcut.<id>rows infaro.db, seeded synchronously from the pre-paint injection;keybindings.tsresolvesoverride ?? default.fileBrowserKeys.tsadds the file-browser catalog (F2 rename & friends),useShortcuts.tsdispatches non-modifier chords, andKeyboardSettings.tsxis the Settings → Keyboard tab (reset-to-default viasettings_delete).Brand icons (Plan 14)
brandIcons.tsxrenders@iconify/react/offlineagainst curated, committed data inbrandIconData.ts(generated byscripts/gen-brand-icons.mjs) — the offline entry point means no name is ever looked up over the network. Protocol logos on the connection rail and vendor logos on the S3/WebDAV provider presets inProfileEditor.tsx.App updater, notifications & PATH install (Plan 16)
updaterStore.tsdrives@tauri-apps/plugin-updater: a 6-hour throttled launch check (persisted vialastUpdateCheckinfaro.db), signature verified by the plugin against the pubkey intauri.conf.json, download-progress state, and relaunch.UpdatePrompt.tsxrenders availability / progress / restart.notifications.ts— OS toasts for transfer-batch completion, a folder-sync pair entering its error state, and an edit-in-place save failure. Unfocused-only, opt-in (default on), lazy permission on first eligible event; clicking focuses the window and opens the relevant panel.path_integration.rs— per-user "Add faro-cli to PATH". Windows editsHKCU\Environment, preserving the value's registry type and de-duping (deliberately neversetx), snapshots the priorPathintofaro.db, and broadcastsWM_SETTINGCHANGE. macOS/Linux symlink into~/.local/binwith a marker-guarded profile line. Removal only ever touches Faro's own entry.AboutSettings.tsxrenders the status row.Extras & infra
ProfileEditor.tsx(no PuTTYgen); the New Connection dialog widened to a two-pane layout that scales with the provider list.lib.rs/commands.rsregister the new command groups and the updater / notification / process plugins;capabilities/default.jsongrants their permissions.release.yml— signs updater artifacts and generateslatest.jsononly whenTAURI_SIGNING_PRIVATE_KEYis set (via a--config updater-release.conf.jsonoverlay), warning-and-continuing otherwise so the pipeline never breaks;docs/updater-key-custody.mddocuments key custody.keyring,aes-gcm,argon2,image, the Tauri updater/notification/process plugins, and@iconify/react;THIRD_PARTY_LICENSES.mdrecords Material Icon Theme and Iconify.src/mock/*gains notification / process / updater / window shims so the browser-only dev build renders the new surfaces; docs add plans 11–17 and refresh the ROADMAP status.