feat(project): Project Explorer backend (runtime, WS monitor, HTTP) - #701
Merged
Conversation
tcp404
force-pushed
the
boii/feat/project-explorer
branch
from
July 28, 2026 15:07
196d882 to
9bef42f
Compare
Stage-0 backend runtime: file: provider, non-recursive watcher, canonical-keyed sparse tree model, subscription registry with grace/warm-LRU, and the serial shard decision core plus debounce and raw-event mapping. Lands in aionui-project's runtime module rather than a separate crate so canonical/containment are reused directly. Adds tokio/async-trait/notify deps and a canonical::uri_to_path helper. Covered by runtime unit tests across pure-logic, real-fs, and timed watcher tiers; platform-specific paths gated by cfg.
Connect the stage-0 fs runtime to the realtime WS channel with the
JSON-RPC monitor protocol (protocol.md), reusing the existing /ws entry
and unicast rather than a new endpoint or transport.
- realtime: add MessageRouter::on_disconnect (default no-op) so stateful
routers release per-connection state; add WebSocketManager
send_to_or_disconnect for ordered streams — a full channel closes the
connection (client reconnects + re-subscribes) instead of silently
dropping a frame, which would desync the gap-free protocol.
- aionui-project/monitor: a single actor owns the shard, debounce buffer
and file runtime; its event loop multiplexes inbound frames, watcher
events, debounce flush and grace reap. Dispatch handles initialize /
fs subscribe|unsubscribe / read|write|mkdir|remove|rename, resolving
pe-relative refs to canonicals, enforcing realpath containment before
IO, and fanning canonical-domain snapshots/deltas out to each
subscriber's own pe-keyed wire target. Wire types stay pub(crate).
- aionui-app: adapt the transport — FsMessageRouter forwards fs frames
and disconnects into the actor, WsManagerPush wraps replies in the
{ name: "fs", data } envelope, spawn_fs_monitor starts the actor and
installs the router.
The outer transport envelope reuses realtime's WebSocketMessage; the
inner JSON-RPC frames follow protocol.md.
- Add aionui-api-types project DTOs (ProjectDetailResponse, ProjectEntry,
AttachFolderRequest); exclude canonical/absolute paths so the client keys
only on pe_id + relative_path
- Add aionui-project routes: GET /api/projects/{id}, POST/DELETE .../folders;
map ProjectError to ApiError with stable domain codes; derive display_path
from resource_uri
- Register the project router + ProjectRouterState in the app composition layer
- Add a top-level project_id to ConversationResponse, populated in the shared
row-to-response mapper
- Broadcast conversation.listChanged(updated) on a lazy None->Some project_id
backfill in get(), so the client reactively refetches the now-bound id
instead of polling for it
- Trace inbound dispatch per frame (debug: method + session); warn on malformed frames and unknown methods - Log subscribe/unsubscribe as lifecycle boundaries (info: target counts) and file commands ok/fail (info/warn: op + pe:rel + error code) - Log connection disconnect and overflow rescan (info), apply and snapshot/delta fan-out (debug: subscriber/entry/change counts), and grace eviction (info: counts only) - Keep payloads safe: never log file contents, write/command bodies, or secrets; only method/session/pe:rel/counts/codes, with absolute canonical URIs confined to debug, so the flow stays diagnosable in production
- tree_model diff synthesized renames by matching removed+added pairs on
inode alone; a same-name file->dir change coalesced into a bogus
Renamed{from:"x",to:"x"} when the OS reused the freed file inode for the
new dir (Linux does, macOS assigns a fresh inode), failing CI on
apply_kind_change_is_remove_plus_add while passing locally
- Require matching kind for rename synthesis: a rename preserves kind, so a
kind change stays remove + add even under an inode collision
- Add a direct diff() unit test (same inode, differing kind) reproducing the
case platform-independently; genuine renames (same kind + inode) unaffected
- Add api-types ChatFileRef tagged union (Project{pe_id,relative_path} |
Upload{path}); the three send DTOs now carry Vec<ChatFileRef>, replacing
raw client absolute-path strings (hard cut, no back-compat)
- Add ProjectService::resolve_chat_message: resolves Project refs via
resolve_reference(Read), requires Upload paths under the managed upload dir
(temp_dir/aionui), and re-inlines resolved absolute paths into content with
the [[AION_FILES]] marker so aionrs/ACP/persistence/UI stay unchanged
- Resolve at the send boundary in conversation + team sends (atomic: one bad
ref fails the whole send); persist and hand the agent the inlined content;
the turn input now carries resolved content+files, not the HTTP DTO
- /api/fs/copy target is now a pe-addressed CopyTarget resolved to an absolute
dir; name collisions are reported as CopyFailure{path,reason} instead of
silently overwriting; directories are files-only this round
- Update the file_e2e copy tests to register a project and POST the
pe-addressed target
- WHY: the client keys only on {pe_id,relative_path}; resolution and
containment happen once at the backend edge
- resolve_chat_message rejected project refs that were not regular files, but the explorer tree allows attaching a folder (right-click a directory), so a folder ref wrongly returned chat_file_missing - Relax the project-ref check to require only that the resolved path exists (file or directory); uploads remain files-only - Add a test for a directory project ref
- Add ChatFileRef::Local { path } third variant for files the user picks
in the backend-machine host-file browser, which already exposes the
whole filesystem
- Resolve local refs by canonicalizing (collapsing symlinks/`..`) and
requiring an existing regular file; no managed-directory restriction,
since that D2 invariant governs the upload channel only and the picker
producing the path already exposes this surface
- Add ProjectError::LocalPathNotReadable (stable code local_path_not_readable,
400) for missing or non-file local paths
- Cover local resolve/reject paths and the local wire form with tests
…ation - Add route-layer tests asserting the From<ProjectError> for ApiError arc: LocalPathNotReadable and UploadPathOutsideRoot each map to HTTP 400 with their stable code strings, which the frontend branches on - Add a symlink test proving local resolve canonicalizes to the target's real path (not the raw link), locking the collapse-symlink behavior
tcp404
force-pushed
the
boii/feat/project-explorer
branch
from
July 29, 2026 07:36
9bef42f to
29d9071
Compare
piorpua
pushed a commit
that referenced
this pull request
Jul 29, 2026
🤖 I have created a release *beep* *boop* --- ## [0.1.54](v0.1.53...v0.1.54) (2026-07-29) ### Features * multi-account user scope isolation ([#669](#669)) ([7f8ed6c](7f8ed6c)) * **project:** Project Explorer backend (runtime, WS monitor, HTTP) ([#701](#701)) ([0a79100](0a79100)) * **scripts:** carry aionrs changelog into the bump PR ([#703](#703)) ([f34cfb6](f34cfb6)) ### Code Refactoring * **acp:** upgrade agent-client-protocol SDK 0.11.1 -> 2.0.0 ([#708](#708)) ([9e185f0](9e185f0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Backend for the Project Explorer (stages 0/1/3), targeting
main.file:provider, non-recursive watcher + overflow signal, sparse TreeModel (mount/apply/diff), subscription registry (forward/reverse reconcile + grace + warm-LRU), single-shard actor (desktop N=1). Underaionui-project/src/runtime/.fs/*JSON-RPC over the realtime WebSocket: initialize, subscribe/unsubscribe (snapshot + delta fan-out), file commands (read/write/mkdir/remove/rename) with realpath containment. The actor stays transport-agnostic (narrow push port); the app layer supplies the socket adapter.GET /api/projects/{id},POST/DELETE /api/projects/{id}/folders. Project DTOs live inaionui-api-typesand deliberately exclude canonical/absolute paths (the client keys only onpe_id+relative_path);ProjectErrormaps toApiErrorwith stable domain codes;display_pathis derived from the folder'sresource_uri.project_idtoConversationResponse; on a lazyNone -> Someproject_id backfill duringget(), broadcastsconversation.listChanged(updated)so the client reactively refetches the now-bound id instead of polling.fs/*pipeline (never logs file contents, write/command bodies, or secrets; absolute URIs confined todebug).Testing
just pushpre-push gate green:cargo build --workspace,clippy -D warnings,fmt, and fullnextest— 7596 passed / 0 failed / 18 skipped.GET/attach/remove return the expected shapes (no canonical leakage); WS subscribe -> on-disk change ->fs/delta;conversation.listChangedemitted on a real project_id backfill and suppressed on already-bound reads.Notes
mainis ahead of this branch's cut point;git merge-tree --write-treereports a clean merge (no conflicts).