Skip to content

feat(project): Project Explorer backend (runtime, WS monitor, HTTP) - #701

Merged
tcp404 merged 9 commits into
mainfrom
boii/feat/project-explorer
Jul 29, 2026
Merged

feat(project): Project Explorer backend (runtime, WS monitor, HTTP)#701
tcp404 merged 9 commits into
mainfrom
boii/feat/project-explorer

Conversation

@tcp404

@tcp404 tcp404 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Backend for the Project Explorer (stages 0/1/3), targeting main.

  • Runtime (stage 0) — single-layer 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). Under aionui-project/src/runtime/.
  • WS monitor (stage 1)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.
  • HTTP control-plane (stage 3)GET /api/projects/{id}, POST/DELETE /api/projects/{id}/folders. Project DTOs live in aionui-api-types and deliberately exclude canonical/absolute paths (the client keys only on pe_id + relative_path); ProjectError maps to ApiError with stable domain codes; display_path is derived from the folder's resource_uri.
  • conversation project_id delivery — adds a top-level project_id to ConversationResponse; on a lazy None -> Some project_id backfill during get(), broadcasts conversation.listChanged(updated) so the client reactively refetches the now-bound id instead of polling.
  • Observability — structured, leveled, redacted logging across the fs/* pipeline (never logs file contents, write/command bodies, or secrets; absolute URIs confined to debug).

Testing

  • just push pre-push gate green: cargo build --workspace, clippy -D warnings, fmt, and full nextest7596 passed / 0 failed / 18 skipped.
  • Live-verified against a running instance: HTTP GET/attach/remove return the expected shapes (no canonical leakage); WS subscribe -> on-disk change -> fs/delta; conversation.listChanged emitted on a real project_id backfill and suppressed on already-bound reads.

Notes

  • Base main is ahead of this branch's cut point; git merge-tree --write-tree reports a clean merge (no conflicts).

@tcp404
tcp404 force-pushed the boii/feat/project-explorer branch from 196d882 to 9bef42f Compare July 28, 2026 15:07
tcp404 added 9 commits July 29, 2026 15:23
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
tcp404 force-pushed the boii/feat/project-explorer branch from 9bef42f to 29d9071 Compare July 29, 2026 07:36
@tcp404
tcp404 merged commit 0a79100 into main Jul 29, 2026
6 checks passed
@tcp404
tcp404 deleted the boii/feat/project-explorer branch July 29, 2026 07:52
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 -&gt; 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>
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.

1 participant