Skip to content

Terminal splits, keychain security, previews, shortcuts, and updates - #6

Merged
jhd3197 merged 116 commits into
mainfrom
dev
Jul 19, 2026
Merged

Terminal splits, keychain security, previews, shortcuts, and updates#6
jhd3197 merged 116 commits into
mainfrom
dev

Conversation

@jhd3197

@jhd3197 jhd3197 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

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 localStorage blob and into the OS keychain (it never crosses the IPC boundary as a value again), app settings graduate to faro.db with 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

  • Split terminal panes on one connection — logs on one side, commands on the other. Panes keep their scrollback and live session through tab switches, dock toggles, and layout changes.
  • Command snippets with {{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.
  • Your Anthropic API key now lives in the OS keychain, not a plaintext file — and it never leaves the backend; Faro reads it only at the moment it calls the API.
  • Encrypted, password-protected backup and restore — every connection, secret, and setting in one file you can move to a new machine, safe to email or drop on a USB stick.
  • Opt-in remote image thumbnails that fetch only what's on screen and cache to disk, so scrolling a folder with 100k images never floods the server.
  • Fully remappable keyboard shortcuts with a new Settings → Keyboard tab, plus F2-to-rename (and friends) in the file browser.
  • Real brand logos on connections and provider presets (AWS, Azure, Cloudflare, Nextcloud, Backblaze…), bundled offline — no icon ever fetched over the network.
  • A signed in-app updater — Faro checks for new releases, shows download progress, and restarts to apply.
  • Generate SSH keys right in the New Connection dialog — no external PuTTYgen step — and add faro-cli to your PATH in one click from Settings → About.
  • Clearer error toasts that tell you what to do ("Reconnect or re-enter your credentials") instead of echoing raw backend text.
Technical changes

Security & portability (Plan 12)

  • credentials.rs — OS-keychain store under the single service com.faro.credentials, keyed by purpose. One-way set_secret/has_secret/delete_secret exposed to the frontend; get_secret is Rust-only and deliberately never a Tauri command. agent.rs fetches the Anthropic key from the keychain at call time. Every write is mirrored into a faro.db manifest (db::record_keychain) so backup knows which entries to export.
  • src/lib/secretMigration.ts — one-time migration of the legacy plaintext faro.settings.v1 API key into the keychain, scrubbing the localStorage field only after a successful write (retries next launch on failure).
  • Settings moved into faro.dbsettings table + settings_get_all / settings_set_all / settings_delete. The main window is built in setup() with an initialization_script that seeds window.__FARO_SETTINGS__ before first paint (no flash-of-defaults). runSettingsMigration migrates-then-verifies before removing the localStorage blob.
  • error.rsFaroError { kind, message } with a kebab-case ErrorKind and a classifier that prefers a concrete io::Error kind anywhere in the cause chain, then falls back to message keywords. src/lib/errors.ts pattern-matches kind into actionable toast copy and treats un-migrated string errors as other, so both paths render sensibly.
  • backup.rs — the FAROBAK\x01 container: 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-safe faro.db snapshot, and every keychain credential. Restore stages <name>.restore files and injects credentials into the keychain; apply_pending_restore swaps 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. Components attach/detach the 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 before set() rather than inside the updater.
  • Snippets — snippetsStore.ts, SnippetsPanel.tsx, VariableDialog.tsx, and snippets.ts: {{variable}} extraction/resolution, a multi-line confirm, a use-count bump on insert, and injection at the focused prompt via termInput.ts (never auto-submitted). Backed by a snippets table in faro.db.

Remote previews & protocol depth (Plan 13)

  • preview.rsPreviewManager: bounded remote read (≤ 25 MiB), decode + downscale via the image crate on a blocking task to a ≤ 160 px PNG, and an LRU disk cache (256 MiB budget) indexed in faro.db by (connection, path, change-signal). Per-connection concurrency cap of 4 behind the frontend's viewport gate.
  • Preview frontend — opt-in toggle (default off for remote backends), IntersectionObserver viewport fetching with cancel-on-scroll-out and a concurrency budget, wired through fileUiAdapter.ts, Thumbnail.tsx, and FilePane.tsx.
  • scp.rs — the rcp-derived SCP wire protocol against a generic AsyncRead + AsyncWrite (download_to execs scp -f, upload_from execs scp -t), status-byte gated, with single-quoted remote paths (quote_path). Wired into SshSession transfer methods as a fallback for hosts that disable the SFTP subsystem.

Keyboard shortcuts (Plan 15)

  • bindingsStore.ts holds only the user's overrides as shortcut.<id> rows in faro.db, seeded synchronously from the pre-paint injection; keybindings.ts resolves override ?? default. fileBrowserKeys.ts adds the file-browser catalog (F2 rename & friends), useShortcuts.ts dispatches non-modifier chords, and KeyboardSettings.tsx is the Settings → Keyboard tab (reset-to-default via settings_delete).

Brand icons (Plan 14)

  • brandIcons.tsx renders @iconify/react/offline against curated, committed data in brandIconData.ts (generated by scripts/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 in ProfileEditor.tsx.

App updater, notifications & PATH install (Plan 16)

  • updaterStore.ts drives @tauri-apps/plugin-updater: a 6-hour throttled launch check (persisted via lastUpdateCheck in faro.db), signature verified by the plugin against the pubkey in tauri.conf.json, download-progress state, and relaunch. UpdatePrompt.tsx renders 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 edits HKCU\Environment, preserving the value's registry type and de-duping (deliberately never setx), snapshots the prior Path into faro.db, and broadcasts WM_SETTINGCHANGE. macOS/Linux symlink into ~/.local/bin with a marker-guarded profile line. Removal only ever touches Faro's own entry. AboutSettings.tsx renders the status row.

Extras & infra

  • In-app SSH key generation in ProfileEditor.tsx (no PuTTYgen); the New Connection dialog widened to a two-pane layout that scales with the provider list.
  • lib.rs / commands.rs register the new command groups and the updater / notification / process plugins; capabilities/default.json grants their permissions.
  • release.yml — signs updater artifacts and generates latest.json only when TAURI_SIGNING_PRIVATE_KEY is set (via a --config updater-release.conf.json overlay), warning-and-continuing otherwise so the pipeline never breaks; docs/updater-key-custody.md documents key custody.
  • Dependencies — keyring, aes-gcm, argon2, image, the Tauri updater/notification/process plugins, and @iconify/react; THIRD_PARTY_LICENSES.md records 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.

jhd3197 and others added 30 commits July 16, 2026 00:09
- 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>
jhd3197 and others added 25 commits July 18, 2026 22:47
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>
Copilot AI review requested due to automatic review settings July 19, 2026 16:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)) {
@jhd3197
jhd3197 merged commit 700a907 into main Jul 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants