Skip to content

feat(projects): cross-app embedding — Files + Messages in ProjectWorkspace#264

Merged
jaylfc merged 7 commits into
masterfrom
feat/projects-cross-app-embedding
Apr 25, 2026
Merged

feat(projects): cross-app embedding — Files + Messages in ProjectWorkspace#264
jaylfc merged 7 commits into
masterfrom
feat/projects-cross-app-embedding

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Apr 25, 2026

Summary

Tasks 25 and 26 from the Projects-app foundation plan — embed FilesApp and MessagesApp inside ProjectWorkspace, with backend route family for project-scoped files and project_id-aware chat channel filtering.

  • FilesApp accepts a rootPath prop ("project:<slug>"); standalone-mode UI unchanged. Backend exposes /api/projects/{slug}/files* mirroring user-workspace routes, rooted at <projects_root>/<slug>/files. 12 new route tests.
  • MessagesApp accepts a scope?: { projectId?: string } prop. When scoped, fetches and creates channels filter by project_id server-side; when standalone, root sidebar sections (DM/topic/group) strictly exclude project-tagged channels and a new collapsible "Projects" section nests them. Cloned-agent channels appear only inside Projects (naturally — they carry the owning project_id). 2 new route tests.
  • ProjectWorkspace gains "Files" and "Messages" tabs. Both embeds use key={project.id} to remount on project change (avoids stale state from lazy useState initialisers).
  • Mobile sidebar a11y — Projects section uses keyboard-accessible expand/collapse mirroring the existing Archived pattern (button + aria-expanded + aria-controls).

Test plan

  • pytest tests/projects tests/test_routes_projects.py tests/test_chat_channels.py tests/test_routes_chat_channels.py tests/test_routes_project_files.py — 94 passed
  • npx tsc -b --noEmit (desktop) — clean
  • Manual smoke: open standalone Messages → Projects collapsible group works on desktop and mobile, root sections do not leak project channels
  • Manual smoke: open a Project → Messages tab shows only that project's channels, channel-create posts the right project_id
  • Manual smoke: open a Project → Files tab browses ~/Projects/<slug>/files, upload/mkdir/delete work, switching projects shows the new project's tree (no stale state)

Summary by CodeRabbit

  • New Features
    • Added Files and Messages tabs in project workspaces.
    • Project-scoped file management: list, watch (live updates), upload, download, mkdir, delete, and stats.
    • Project-scoped chat channels: create channels with project association and filter channel lists by project.
  • Tests
    • New end-to-end tests covering project file operations, stats, validations, and project-scoped channel APIs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

Adds project-scoped file and messaging capabilities: frontend apps accept project roots/scopes and ProjectWorkspace exposes files/messages tabs; backend gains project-scoped chat filters and a new project files router with listing, watch (SSE), upload, mkdir, download, delete, and stats endpoints; tests added for both areas.

Changes

Cohort / File(s) Summary
Frontend App Changes
desktop/src/apps/FilesApp.tsx, desktop/src/apps/MessagesApp.tsx, desktop/src/apps/ProjectsApp/ProjectWorkspace.tsx
FilesApp gains optional rootPath prop and project: location parsing for file-browser endpoints; MessagesApp accepts optional scope.projectId to scope channel listing/creation and adjust UI grouping; ProjectWorkspace adds files and messages tabs that instantiate these apps with project-scoped props.
New Project Files Router
tinyagentos/routes/project_files.py
New FastAPI router exposing project-scoped file APIs: list, SSE watch, multipart upload, mkdir, download, delete, and stats; includes slug validation and path traversal protection; exports router and request model(s).
Chat Route Updates
tinyagentos/routes/chat.py
GET /api/chat/channels accepts optional project_id filter; POST /api/chat/channels forwards project_id from request body into channel creation.
App Router Registration
tinyagentos/app.py
Catalog reload now registers project_files routes alongside existing route registrations.
Backend Tests: Chat
tests/test_routes_chat_channels.py
New async tests verifying channel creation with project_id and GET filtering by project_id returns only matching channels.
Backend Tests: Project Files
tests/test_routes_project_files.py
New comprehensive tests for project files endpoints: invalid slug handling, listing, mkdir, upload, download (byte-accurate), delete, stats, path traversal rejection, and conflict responses.

Sequence Diagram

sequenceDiagram
    participant Client as Client / ProjectWorkspace
    participant FilesApp as FilesApp Component
    participant Backend as Backend API
    participant FS as File System

    Client->>FilesApp: mount with rootPath="project:slug"
    FilesApp->>Backend: GET /api/files/project/{slug}?path=...
    Backend->>FS: read directory
    FS-->>Backend: entries + metadata
    Backend-->>FilesApp: JSON entries
    FilesApp-->>Client: render list

    Client->>FilesApp: request watch /api/files/project/{slug}/watch
    FilesApp->>Backend: open SSE
    loop poll interval
      Backend->>FS: compute signature
      FS-->>Backend: signature
      Backend-->>FilesApp: SSE update (if changed)
    end

    Client->>FilesApp: upload file (multipart)
    FilesApp->>Backend: POST /upload
    Backend->>FS: create dirs & write file
    FS-->>Backend: success + size
    Backend-->>FilesApp: upload response
    FilesApp-->>Client: refresh list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped to tidy project ground,

Paths and channels, neatly found.
Files that watch and messages grouped,
New tabs opened, work resumed and looped.
A carrot for each changed route found! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: embedding FilesApp and MessagesApp into ProjectWorkspace with cross-app support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/projects-cross-app-embedding

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src/apps/FilesApp.tsx`:
- Around line 398-405: FilesApp initializes location from rootPath but leaves
selectedLocation null, so mobile view lands on the list pane and the project
root isn’t reachable; update the selectedLocation state initializer to mirror
location when rootPath is provided (i.e., initialize selectedLocation to
rootPath ?? "workspace" or to the same derived value as location) so
MobileSplitView / MobileDetailPane opens the project detail pane on mobile;
locate the selectedLocation state (and any setSelectedLocation usage) and change
its useState initial value to match the existing location initialization in
FilesApp.

In `@tinyagentos/routes/project_files.py`:
- Around line 65-67: _dir_signature currently casts entry['modified'] to int
which drops sub-second precision and can cause identical signatures for rapid
edits; update _dir_signature to preserve fractional seconds (e.g., use the float
value or format with sufficient precision like 6+ decimals) when building the
signature parts so changes within the same second produce different signatures;
locate _dir_signature and replace the int(...) conversion with the
preserved/rounded float formatting (e.g., using the raw timestamp or formatted
float) for the modified field.
- Around line 144-160: The api_project_mkdir handler should check for name
collisions with existing files before calling target.mkdir: after resolving
target with _resolve_safe (and after current validations), add a check using
target.exists() / target.is_file() and if the path exists and is a file return
JSONResponse({"error": "path exists and is a file"}, status_code=400); if it
already exists and is a directory you can return the same success response (or a
200), otherwise call target.mkdir(parents=True, exist_ok=True) as before. Ensure
you reference api_project_mkdir, _resolve_safe, workspace, target, and body.path
when making the changes.
- Around line 121-141: The upload handler api_project_upload_file should
validate that the resolved path is a directory (not an existing file) before
calling mkdir and must stream the incoming upload to disk instead of buffering
it in memory; after calling _resolve_safe(workspace, path) ensure target_dir
exists and if target_dir exists and is_file() return a 400 "Invalid path" error,
otherwise create directories; then write the incoming UploadFile to disk by
streaming from UploadFile.file (or by reading in fixed-size chunks via await
file.read(chunk_size)) into dest opened in binary write mode (or use
shutil.copyfileobj on file.file) so the whole file is not loaded into RAM, and
ensure you close the source/dest file handles when done.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b9f087bc-6215-4304-acf6-b3036b3488ea

📥 Commits

Reviewing files that changed from the base of the PR and between e0927d6 and 1b92ec7.

📒 Files selected for processing (8)
  • desktop/src/apps/FilesApp.tsx
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/ProjectsApp/ProjectWorkspace.tsx
  • tests/test_routes_chat_channels.py
  • tests/test_routes_project_files.py
  • tinyagentos/app.py
  • tinyagentos/routes/chat.py
  • tinyagentos/routes/project_files.py

Comment thread desktop/src/apps/FilesApp.tsx
Comment thread tinyagentos/routes/project_files.py
Comment on lines +121 to +141
@router.post("/api/projects/{slug}/files/upload")
async def api_project_upload_file(request: Request, slug: str, path: str = "", file: UploadFile = File(...)):
"""Upload a file to the project's files folder."""
workspace = _get_project_files_root(request, slug)
if workspace is None:
return JSONResponse({"error": "Invalid slug"}, status_code=400)

if path:
target_dir = _resolve_safe(workspace, path)
if target_dir is None:
return JSONResponse({"error": "Invalid path"}, status_code=400)
target_dir.mkdir(parents=True, exist_ok=True)
else:
target_dir = workspace

filename = Path(file.filename).name
dest = target_dir / filename
content = await file.read()
dest.write_bytes(content)
rel = dest.relative_to(workspace)
return {"name": filename, "path": str(rel), "size": len(content), "status": "uploaded"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the upload target and avoid buffering the whole file in memory.

If path already resolves to a file, target_dir.mkdir(..., exist_ok=True) raises FileExistsError, so this handler returns a 500 instead of a client error. The following await file.read() also loads the entire upload into RAM, which is risky for large project files.

Proposed fix
 `@router.post`("/api/projects/{slug}/files/upload")
 async def api_project_upload_file(request: Request, slug: str, path: str = "", file: UploadFile = File(...)):
@@
     if path:
         target_dir = _resolve_safe(workspace, path)
         if target_dir is None:
             return JSONResponse({"error": "Invalid path"}, status_code=400)
+        if target_dir.exists() and not target_dir.is_dir():
+            return JSONResponse({"error": "Upload path must be a directory"}, status_code=409)
         target_dir.mkdir(parents=True, exist_ok=True)
     else:
         target_dir = workspace

     filename = Path(file.filename).name
     dest = target_dir / filename
-    content = await file.read()
-    dest.write_bytes(content)
+    with dest.open("wb") as fh:
+        while chunk := await file.read(1024 * 1024):
+            fh.write(chunk)
     rel = dest.relative_to(workspace)
-    return {"name": filename, "path": str(rel), "size": len(content), "status": "uploaded"}
+    return {"name": filename, "path": str(rel), "size": dest.stat().st_size, "status": "uploaded"}
🧰 Tools
🪛 Ruff (0.15.11)

[warning] 122-122: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tinyagentos/routes/project_files.py` around lines 121 - 141, The upload
handler api_project_upload_file should validate that the resolved path is a
directory (not an existing file) before calling mkdir and must stream the
incoming upload to disk instead of buffering it in memory; after calling
_resolve_safe(workspace, path) ensure target_dir exists and if target_dir exists
and is_file() return a 400 "Invalid path" error, otherwise create directories;
then write the incoming UploadFile to disk by streaming from UploadFile.file (or
by reading in fixed-size chunks via await file.read(chunk_size)) into dest
opened in binary write mode (or use shutil.copyfileobj on file.file) so the
whole file is not loaded into RAM, and ensure you close the source/dest file
handles when done.

Comment thread tinyagentos/routes/project_files.py
- mobile FilesApp now opens the project location directly (was unreachable)
- preserve sub-second precision in watch signature
- upload + mkdir routes return 400 instead of 500 on file/dir collision
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
tinyagentos/routes/project_files.py (1)

138-143: ⚠️ Potential issue | 🟠 Major

Stream uploads and guard destination filename collisions to prevent 500s.

The handler still buffers the entire upload in memory, and dest.write_bytes(...) can raise on directory/empty-name collisions, returning server errors instead of client-safe 400 responses.

Proposed fix
-    filename = Path(file.filename).name
-    dest = target_dir / filename
-    content = await file.read()
-    dest.write_bytes(content)
-    rel = dest.relative_to(workspace)
-    return {"name": filename, "path": str(rel), "size": len(content), "status": "uploaded"}
+    filename = Path(file.filename or "").name
+    if not filename:
+        return JSONResponse({"error": "Invalid filename"}, status_code=400)
+
+    dest = target_dir / filename
+    if dest.exists() and dest.is_dir():
+        return JSONResponse({"error": "Path conflicts with an existing directory"}, status_code=400)
+
+    size = 0
+    with dest.open("wb") as fh:
+        while chunk := await file.read(1024 * 1024):
+            fh.write(chunk)
+            size += len(chunk)
+    await file.close()
+
+    rel = dest.relative_to(workspace)
+    return {"name": filename, "path": str(rel), "size": size, "status": "uploaded"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tinyagentos/routes/project_files.py` around lines 138 - 143, The current
handler buffers the entire upload via content = await file.read() and calls
dest.write_bytes(...) which can OOM and raise unhandled errors; change it to
stream the incoming file into dest using the UploadFile.file stream (e.g., copy
chunks from file.file into dest.open("wb")) to avoid buffering, validate
Path(file.filename).name is non-empty and sanitized (no path traversal) before
creating dest, ensure dest.resolve() is still under workspace.resolve() and
check if dest.exists() to return a client-safe 400 for bad filenames/collisions
instead of letting dest.write_bytes raise, and wrap filesystem operations around
specific exception handling to convert expected filesystem/client errors to 400
responses while letting unexpected errors propagate as 500s.
🧹 Nitpick comments (1)
desktop/src/apps/FilesApp.tsx (1)

163-168: Update the root-kinds helper comment to include project scope.

The comment still says there are three root kinds, but the implementation now supports project roots as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/FilesApp.tsx` around lines 163 - 168, The comment describing
supported workspace root kinds is out of date — update the "root-kinds" helper
comment (the comment above the function that builds endpoint URLs for workspace
roots) to mention project-scoped roots as well (e.g., "project:<id>" or
"project" depending on your implementation) and adjust the list/count to reflect
four root kinds: workspace, agent:<slug>, project (project-scoped), and
<folder-name> (shared folder); keep the wording consistent with the existing
style so fetch/upload/mkdir/delete/watch consumers remain clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tinyagentos/routes/project_files.py`:
- Around line 138-143: The current handler buffers the entire upload via content
= await file.read() and calls dest.write_bytes(...) which can OOM and raise
unhandled errors; change it to stream the incoming file into dest using the
UploadFile.file stream (e.g., copy chunks from file.file into dest.open("wb"))
to avoid buffering, validate Path(file.filename).name is non-empty and sanitized
(no path traversal) before creating dest, ensure dest.resolve() is still under
workspace.resolve() and check if dest.exists() to return a client-safe 400 for
bad filenames/collisions instead of letting dest.write_bytes raise, and wrap
filesystem operations around specific exception handling to convert expected
filesystem/client errors to 400 responses while letting unexpected errors
propagate as 500s.

---

Nitpick comments:
In `@desktop/src/apps/FilesApp.tsx`:
- Around line 163-168: The comment describing supported workspace root kinds is
out of date — update the "root-kinds" helper comment (the comment above the
function that builds endpoint URLs for workspace roots) to mention
project-scoped roots as well (e.g., "project:<id>" or "project" depending on
your implementation) and adjust the list/count to reflect four root kinds:
workspace, agent:<slug>, project (project-scoped), and <folder-name> (shared
folder); keep the wording consistent with the existing style so
fetch/upload/mkdir/delete/watch consumers remain clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c08798ee-8511-4778-81c3-cf013227458b

📥 Commits

Reviewing files that changed from the base of the PR and between 1b92ec7 and 85e1fd4.

📒 Files selected for processing (3)
  • desktop/src/apps/FilesApp.tsx
  • tests/test_routes_project_files.py
  • tinyagentos/routes/project_files.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_routes_project_files.py

@jaylfc jaylfc merged commit cfd46a7 into master Apr 25, 2026
7 of 8 checks passed
@jaylfc jaylfc deleted the feat/projects-cross-app-embedding branch April 25, 2026 17:57
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