Skip to content

Remove built-in agent chat; fix SFTP breadcrumb navigation on relative paths - #7

Merged
jhd3197 merged 119 commits into
mainfrom
dev
Jul 20, 2026
Merged

Remove built-in agent chat; fix SFTP breadcrumb navigation on relative paths#7
jhd3197 merged 119 commits into
mainfrom
dev

Conversation

@jhd3197

@jhd3197 jhd3197 commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Two focused changes on top of the v1.3.24 release batch. The big one is a removal: the built-in AI agent chat is gone — the Tauri commands, the React panel, the IPC surface, and the Zustand stores behind it, along with the Anthropic-specific API-key settings and the legacy plaintext-key migration path. What stays is the generic keychain/backup plumbing introduced in the last batch, now keyed purely around app service keys rather than a single hardcoded provider. The second change is a correctness fix in the file browser: SFTP connections whose profile has no start directory land on a relative remote path (. / ./public_html/...), and the breadcrumb bar and Up button were silently absolutizing it — producing /./... paths that don't exist, so every breadcrumb click and Up press failed with an sftp read_dir error. Both now keep relative paths relative.

Highlights

  • Built-in agent chat removed — no more chat panel, no Anthropic key field in Settings, no agent commands in the IPC layer. 1,000+ lines deleted across 15 files.
  • Keychain & backup stay, minus the provider lock-in — the credential store and encrypted backup remain fully generic over app service keys; only the Anthropic-specific wiring was removed.
  • SFTP breadcrumb/Up navigation works without a start directory — profiles that open on the server's default home (a relative path) now navigate correctly instead of erroring on every crumb click.
Technical changes

Agent chat removal (42b7fef)

  • src-tauri/src/agent.rs deleted (493 lines) — the Anthropic-calling backend, its Tauri commands, and its registration in lib.rs / commands.rs.
  • Frontend: AgentChat.tsx, agentChatStore.ts, the chat entries in layoutStore.ts, and the IPC wrappers in lib/ipc.ts removed; App.tsx no longer mounts the panel.
  • Settings.tsx drops the Anthropic API-key section; secretMigration.ts loses the legacy plaintext-key migration path that only existed to feed the chat feature.
  • credentials.rs / backup.rs / db.rs adjusted to keep the keychain and backup code generic around app service keys rather than the removed provider.

Relative SFTP path navigation (5c8f438)

  • packages/file-ui/src/components/FilePane.tsxparseSegments gains a relative-POSIX branch: no root crumb, segments accumulate from . instead of being prefixed with /. goUp detects relative paths, stays relative when ascending, and stops at the relative root (.) instead of jumping to /.

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 23:42
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>
When a profile has no start directory the remote path is relative
("." / "./public_html/..."). parseSegments and goUp assumed absolute
POSIX paths, turning breadcrumbs into "/./..." and Up into "/." —
both nonexistent, so every breadcrumb click failed with sftp read_dir.

Keep relative paths relative in both: crumbs accumulate from "." and
goUp stops at the relative root instead of jumping to "/".
Drop the built-in AI chat across the Tauri commands, React UI, IPC layer, and Zustand stores. This also removes the Anthropic-specific API key settings and legacy secret migration path, while keeping the remaining keychain/backup code generic around app service keys.
Copilot AI review requested due to automatic review settings July 20, 2026 02:09

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 removes the built-in AI agent chat surface and adjusts file-browser path handling so SFTP sessions that start on relative remote paths (. / ./…) can navigate via breadcrumbs/Up without generating invalid /./… paths. In addition, the diff introduces a sizable batch of “Plan” work (updater, notifications, shortcut remapping, snippets, brand icons, remote previews, structured errors, backup/restore, etc.), which significantly broadens the PR’s functional scope beyond the title.

Changes:

  • Remove the built-in agent chat feature across frontend state/UI and backend IPC surface.
  • Fix breadcrumb/Up navigation logic for relative POSIX paths (SFTP default home behavior).
  • Add multiple new subsystems and UX surfaces (updater + notifications + shortcut overrides + snippets + brand icons + remote previews + structured IPC errors + backup/restore + path integration + verification scripts).

Reviewed changes

Copilot reviewed 95 out of 98 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
vite.config.ts Adds Vite mock aliases for additional Tauri APIs/plugins used by new frontend features.
THIRD_PARTY_LICENSES.md Documents Iconify icon sources and includes Apache 2.0 license text for mdi set usage.
src/stores/updaterStore.ts New Zustand store driving in-app updater checks/download/relaunch flow.
src/stores/snippetsStore.ts New snippets store for listing/editing/inserting command snippets into terminals.
src/stores/layoutStore.ts Removes agent-chat layout state and toggles.
src/stores/diskScanStore.ts Switches delete error to structured toastError handling.
src/stores/connectionsStore.ts Switches connect error handling to structured toastError + messageOf.
src/stores/bindingsStore.ts New shortcut override store persisted via settings (shortcut.<id> keys).
src/stores/agentChatStore.ts Deletes the built-in agent chat store (feature removal).
src/mock/window.ts Extends mock window API with focus controls for headless verification.
src/mock/updater.ts Adds mock updater plugin implementation for demo/headless flows.
src/mock/process.ts Adds mock process plugin implementation (relaunch/exit flags).
src/mock/notification.ts Adds mock notification plugin implementation (records sent notifications).
src/mock/demo.ts Exposes additional stores/utilities on window.__demo for headless scripts.
src/mock/data.ts Extends mock remote FS dataset with an image-heavy uploads folder.
src/mock/core.ts Expands mock IPC surface for settings/credentials/snippets/path + improves terminal id handling.
src/mock/app.ts Adds mock @tauri-apps/api/app calls (name/version/tauri version).
src/main.tsx Makes theme application conditional since main window theme is now injected pre-paint by Rust.
src/lib/types.ts Adds PathStatus, Snippet, and BackupSummary types for new features.
src/lib/termSuggest.ts Adds optional swallowKey hook so app-level chords can be suppressed in xterm.
src/lib/termInput.ts Introduces focused-terminal registry for snippet injection and other “write to active pane” features.
src/lib/terminalChords.ts Centralizes terminal split/zoom/close chords and provides xterm swallow matcher honoring remaps.
src/lib/snippets.ts Implements {{variable}} extraction/resolution and newline stripping for snippet insertion.
src/lib/secretMigration.ts Adds one-time localStorage → faro.db settings migration logic.
src/lib/notifications.ts Adds curated desktop notification wiring gated by focus + settings.
src/lib/keybindings.ts Adds effective-binding resolver and hook to return commands with override-aware combos.
src/lib/ipc.ts Removes agent chat IPC and adds snippets/settings/credentials/backup/path/thumbnail IPC wrappers.
src/lib/fileUiAdapter.ts Adds remote thumbnail pipeline (gated + concurrency-limited) and adapts file-ui thumbnail API to include abort signals.
src/lib/fileBrowserKeys.ts Adds remappable file-browser action catalog and computed action→combo map.
src/lib/errors.ts Introduces structured IPC error helpers (kindOf, messageOf, toastError).
src/lib/commands.tsx Adds snippets commands and terminal split/zoom/close commands to registry.
src/lib/brandIcons.tsx Adds offline Iconify wrapper and protocol→icon mapping for brand/protocol logos.
src/hooks/useShortcuts.ts Switches to override-resolved commands and adds bare-key dispatch with editable-target guards.
src/components/VariableDialog.tsx Adds snippet variable/multiline confirmation dialog UI.
src/components/UpdatePrompt.tsx Adds app-root mounted update prompt bar that performs throttled launch checks.
src/components/TitleBar.tsx Displays override-aware shortcut labels in title bar menus.
src/components/ServerRail.tsx Uses brand/protocol logos for connection bubbles and search rows.
src/components/ProfileEditor.tsx Uses brand/protocol/vendor logos in protocol picker and provider preset buttons.
src/components/KeyboardShortcutsDialog.tsx Switches to resolved commands and includes file-browser key rows derived from the catalog.
src/components/FileUiBridge.tsx Threads remote preview toggle + file-browser key bindings into @faro/file-ui settings.
src/components/CommandPalette.tsx Switches palette source to resolved commands (effective combos).
src/components/AgentChat.tsx Deletes the built-in agent chat UI (feature removal).
src/App.tsx Removes chat mounting and adds settings migration + notifications init + updater prompt + snippets host.
src-tauri/updater-release.conf.json Adds config overlay enabling updater artifacts in signing-enabled builds.
src-tauri/tauri.conf.json Moves window creation into Rust setup and configures updater endpoints/pubkey/install mode.
src-tauri/src/session/mod.rs Adds SFTP availability probe and SCP upload/download helpers on SSH sessions.
src-tauri/src/lib.rs Removes agent module, adds new subsystems/plugins/commands, adds pre-paint settings injection, and constructs main window in setup.
src-tauri/src/error.rs Adds structured error type/kind classification for IPC boundary.
src-tauri/src/credentials.rs Adds keychain-backed service credential helpers (write/has; read is Rust-only).
src-tauri/faro-cli/src/main.rs Adds encrypted backup export/import commands and FARO_DATA_DIR override support.
src-tauri/Cargo.toml Adds dependencies for notifications/updater/process/preview/backup/path integration.
src-tauri/capabilities/default.json Grants capabilities for notification/updater/process plugins.
scripts/verify-updater.mjs Adds headless verification for updater UX via mock build.
scripts/verify-terminal.mjs Adds headless verification for terminal splits and snippets UI.
scripts/verify-previews.mjs Adds headless verification for remote image preview toggling and rendering.
scripts/verify-path-integration.mjs Adds headless verification for PATH integration UI flow in mock mode.
scripts/verify-notifications.mjs Adds headless verification for notification gating and event triggers in mock mode.
scripts/verify-errors.mjs Adds headless verification for structured error toast behavior.
scripts/gen-brand-icons.mjs Adds generator for curated offline Iconify data file.
packages/file-ui/src/lib/keys.ts Adds file-ui package-level default keybindings and combo matcher.
packages/file-ui/src/index.ts Re-exports file-browser key types/defaults/matcher from the package.
packages/file-ui/src/context.tsx Extends adapter/settings contracts for abortable thumbnail loading and keybinding/remote-preview settings.
packages/file-ui/src/components/Thumbnail.tsx Adds AbortController cancellation for thumbnail loads on unmount/scroll-out.
package.json Adds Iconify deps and new generation script; adds Tauri plugin deps used by new features.
docs/updater-key-custody.md Documents updater signing key custody and CI secret setup.
docs/plans/99_scoped-connection-sharing.md Renames and marks plan as out-of-scope.
docs/plans/17_transfer-queue-depth.md Adds new Plan 17 spec document.
docs/plans/16_app-updater-and-notifications.md Adds Plan 16 spec document for updater/notifications/PATH.
docs/plans/15_keyboard-shortcuts.md Adds Plan 15 spec document for remappable shortcuts and file-browser keys.
docs/plans/14_iconify-brand-icons.md Updates Plan 14 status and expands spec details for Iconify-based brand icons.
docs/plans/13_remote-previews-and-protocol-depth.md Adds Plan 13 spec document (remote previews + protocol depth).
docs/plans/12_security-settings-and-portability.md Adds Plan 12 spec document (keychain/settings/errors/backup).
docs/plans/11_terminal-depth-and-snippets.md Adds Plan 11 spec document (terminal depth + snippets).
CLAUDE.md Updates referenced plan-number range.
.gitignore Ignores .updater/ to prevent committing updater signing private key.
.github/workflows/release.yml Conditionally enables updater artifact signing/config overlay based on repo secrets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +60
// 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)) {
e.preventDefault();
submit();
Comment thread src/lib/snippets.ts
Comment on lines +29 to +31
return body.replace(PLACEHOLDER, (whole, name: string) =>
name in values ? values[name] : whole
);
Comment on lines +15 to +18
const OUT =
process.env.SHOT_DIR ||
"C:/Users/Juan/AppData/Local/Temp/claude/C--Users-Juan-Documents-GitHub-Faro/f1093608-9381-404b-a304-c789e9021bc6/scratchpad";
const PORT = 1425;
Comment thread src/App.tsx
Comment on lines 8 to 10
import { CliUpdatePrompt } from "./components/CliUpdatePrompt";
import { UpdatePrompt } from "./components/UpdatePrompt";
import { Settings } from "./components/Settings";
Resolve conflicts from the squashed security batch vs. the agent-chat
removal: keep dev's state (agent chat removed, keychain/backup generic),
keep main's v1.3.24 version bumps.
@jhd3197
jhd3197 merged commit d5a6fa4 into main Jul 20, 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