Skip to content

fix: drag-and-drop document upload in browser mode (#728) - #729

Merged
kovtcharov merged 9 commits into
mainfrom
kalin/fix-728-document-upload
Apr 10, 2026
Merged

kovtcharov merged 9 commits into
mainfrom
kalin/fix-728-document-upload

Conversation

@kovtcharov

@kovtcharov kovtcharov commented Apr 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #728.

What was broken

Drag-and-drop document upload failed with "Indexing error: Not found. The file or folder may have been moved or deleted." in any browser, and in the packaged Electron desktop app (Electron 40).

Root cause: src/gaia/apps/webui/src/components/DocumentLibrary.tsx:265 relied on (file as any).path, an Electron-only augmentation that's undefined in every browser and was removed in Electron 32+. The fallback file.name (bare filename, no directory) was POSTed to /api/documents/upload-path, which couldn't resolve it → HTTP 404 → misleading frontend error.

Fix

New POST /api/documents/upload endpoint that accepts the file content as multipart form data, streams it to ~/.gaia/documents/<shortid>_<original_name>.<ext> with a 20 MB cap, dedupes by SHA-256 hash, and indexes via the standard RAG pipeline. Drag-and-drop in both DocumentLibrary and ChatView now use this endpoint. Delete cleans up server-owned files; user-owned files from upload-path are left alone.

Also fixes getFriendlyError() in api.ts to prefer the backend's detail over the canned 404 string, so future upload errors surface meaningful messages.

Details worth knowing

  • On-disk filename embeds the original stem (<12-hex-id>_<sanitized_name>.<ext>) so the agent LLM and RAG tools can recognize the file by its human-readable name. A pure-UUID filename caused the LLM to hallucinate paths (e.g. C:\Users\14255\.gaia\documents\<uuid>.pdf → /Users/14255/Documents/<uuid>.pdf) and fail tool lookups.
  • Filename sanitizer strips Windows-illegal chars (<>:"/\|?*), control chars, leading/trailing whitespace and dots, and caps length at 80 chars — keeps total path well under Windows MAX_PATH.
  • 12-hex-char ID instead of full UUID. We dedup by SHA-256 first, so identical-content collisions are impossible; the only theoretical collision is concurrent uploads of different content drawing the same ID, with probability ~1 in 1.4e14 even at 1M concurrent uploads. Saves 24 chars per filename.
  • Reliability: streaming write with abort-on-overflow, try/finally partial cleanup (survives ClientDisconnect), atomic os.replace, orphan cleanup on indexing/db/race failures.
  • Chat-view drop UX: dropping onto the chat area indexes quietly without forcing the Document Library side panel open. Dropping onto the library panel itself still opens it (correct for users intentionally managing their library).

Bundled cleanup

This PR also includes a doc-consistency pass auto-applied by util/lint.py --all --fix (step 8) that aligns the supported-extension allowlist across backend (utils.py, files.py), frontend (UnsupportedFeature.tsx), and tests.

What's NOT changed

  • POST /api/documents/upload-path is untouched — still used by the server-side file browser in FileBrowser.tsx, where the user explicitly picks a real filesystem path.
  • The Electron preload — deliberately not adding webUtils.getPathForFile since it's broken for drag-drop in Electron 30-33+ and the blob path works everywhere.

Known limitation / follow-up

The agent LLM still occasionally cites the on-disk filename (e.g. 9c59c981f8e7_report.pdf) when answering questions about a document, instead of the user's display name (report.pdf). This is because src/gaia/agents/chat/tools/rag_tools.py returns chunk metadata using raw filepaths from self.rag.indexed_files rather than translating them to the display name stored in documents.filename. This is a separate, pre-existing concern in the RAG tool layer and fixing it requires a focused refactor of rag_tools.py (~1800 lines, touches query_documents, query_specific_file, summarize_document, search_indexed_chunks, list_indexed_documents). Tracking it as a follow-up rather than expanding this PR's scope.

Conflict notes for reviewers


Steps to reproduce the bug (before this fix)

One-time setup

git checkout main
uv venv
uv pip install -e ".[dev,ui]"
cd src/gaia/apps/webui && npm install && npm run build && cd -

Windows PowerShell users: quote the extras → uv pip install -e ".[ui]". Without quotes PowerShell silently strips them.

Start the services

# Terminal 1
lemonade-server serve

# Terminal 2
gaia chat --ui

Reproduce

  1. Open the UI in Firefox (most reliable repro; the bug also reproduces in Chrome and the packaged Electron 40 desktop app).
  2. Open DevTools → Network tab, filter on documents.
  3. Open or create a chat session.
  4. Drag any .pdf/.txt/.md/.docx from your file manager onto the Document Library drop zone or the chat area.
  5. Observe:
    • UI: "Indexing error: Not found. The file or folder may have been moved or deleted."
    • Network: POST /api/documents/upload-path → HTTP 404
    • Request body: {"filepath":"report.pdf"} (bare filename — the smoking gun)
    • Backend log: HTTPException: File not found: report.pdf

Verify the fix

git checkout kalin/fix-728-document-upload
cd src/gaia/apps/webui && npm run build && cd -
# Restart `gaia chat --ui`

Repeat the drop. Expected:

  • Network: POST /api/documents/upload (multipart, not upload-path) → HTTP 200
  • File persisted at ~/.gaia/documents/<shortid>_report.pdf with the original name embedded
  • Backend log: Blob upload indexed: report.pdf (<N> bytes, <M> chunks)
  • Drop the same file twice → Blob upload dedup: report.pdf matches existing doc <id> (no second file on disk)
  • Ask the agent about the doc → answers correctly using the indexed content

Test plan

  • 10 new integration tests (tests/integration/test_documents_router.py): happy path, dedup, empty/oversized/bad-extension/illegal-chars rejection, indexing failure cleanup, delete unlinks server-owned, delete preserves user-owned, 404 on delete
  • Adjacent regression sweep: 186 tests pass (files router, folder indexing, database, server unit tests)
  • python util/lint.py --all --fix clean
  • tsc --noEmit clean (no new errors in changed files)
  • End-to-end smoke test via TestClient (upload → list → delete → file unlinked)
  • Manual (Windows + Firefox): drag-and-drop indexes successfully → ✅ validated
  • Manual: dropped doc auto-attaches to chat session → ✅ validated
  • Manual: dedup short-circuit verified → ✅ validated
  • Manual: delete cleanup verified for both server-owned and user-owned files → ✅ validated
  • Manual: agent answers questions correctly using the indexed document → ✅ validated (display-name citation polish tracked as follow-up; see "Known limitation" above)

kovtcharov and others added 4 commits April 9, 2026 13:57
Drag-and-drop document upload fails with "Not found. The file or folder
may have been moved or deleted." because the frontend relied on the
Electron-only `File.path` property, which is `undefined` in all browsers
and was also removed in Electron 32+. The bare filename was then sent
to `/api/documents/upload-path`, which couldn't locate the file and
returned HTTP 404.

Add a new `POST /api/documents/upload` endpoint that accepts the file
content directly as multipart form data, streams it into
`~/.gaia/documents/<uuid>.<ext>` with an abort-on-overflow size check
(20 MB cap), dedupes by SHA-256 content hash, and indexes it through
the standard RAG pipeline. Partial and orphan files are cleaned up on
every failure path (ClientDisconnect, oversized, bad extension,
indexing failure, concurrent-drop race).

`DELETE /api/documents/{id}` now also unlinks the on-disk file when
it lives under the server-managed documents dir, while leaving
user-owned files from `upload-path` untouched.

Adds `ChatDatabase.get_document_by_hash()` for the dedup short-circuit
and 9 integration tests covering happy path, dedup, empty/oversized/
bad-extension rejection, indexing failure cleanup, and delete cleanup
for both server-owned and user-owned documents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Document Library and ChatView drag-and-drop handlers relied on
`(file as any).path` which is `undefined` in browsers and was removed
in Electron 32+, causing drag-and-drop to fail with a confusing
"Not found" error. Switch both handlers to the new
`uploadDocumentBlob()` helper that POSTs the file content directly
as multipart form data to `/api/documents/upload`.

DocumentLibrary: refactor the shared upload state-machine
(preflight/setUploadError/attach/refresh) into a `runUpload` helper
so both the path-based flow (used by the folder input and file
browser) and the new blob flow can share it cleanly.

ChatView: swap the drag-drop handler to call `uploadDocumentBlob`
directly; path-based upload is still used by the server-side file
browser picker where a real filesystem path is available.

Also fix the 404 error-message mapping in `getFriendlyError()` — it
now prefers the backend's `detail` over the previous canned
"The file or folder may have been moved or deleted." string, which
was misleading for upload/indexing flows. 403 and 413 gain the same
detail-aware treatment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Document the two document upload endpoints (`upload-path` vs.
  `upload`) in `docs/spec/agent-ui-server.mdx`, explaining when each
  is used, how the blob endpoint streams to `~/.gaia/documents/` with
  hash-based dedup, and how server-owned files are cleaned up on
  delete.
- Remove the per-PR concurrency group from the Claude workflow's
  `pr-comment` and `issue-handler` jobs so bursts of `@claude`
  mentions on different topics each run to completion instead of
  cancelling each other. These jobs only read diffs and post
  comments, so parallel runs are safe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the `final_path.stat()` call for `file_mtime` inside the
indexing try/except block so a rare filesystem hiccup between the
atomic rename and the stat doesn't leak the final file. The stat
failure will now trigger the same cleanup path as indexing/db
failures, keeping the disk tidy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kovtcharov and others added 4 commits April 9, 2026 14:38
Dropping a file onto the chat view was auto-opening the Document
Library side panel, which interrupts the chat flow when the user
just wants the file indexed and attached to the current session.

Remove the `setShowDocLibrary(true)` call from the ChatView drop
handler. The file still indexes via the blob upload endpoint and
auto-attaches to the current session — users see the new doc appear
in the session's attached-docs row without the panel popping open.
The library can still be opened manually for progress on large files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous blob endpoint wrote files as pure UUID (e.g.
``7df3d195-...pdf``), which caused the agent LLM to hallucinate
"cleaner" paths when referencing the document in tool calls — e.g.
``C:\Users\14255\.gaia\documents\<uuid>.pdf`` became
``/Users/14255/Documents/<uuid>.pdf`` in LLM-generated tool args,
and the RAG tools' substring-based file lookup then failed with
"Document not found in indexed documents."

Embed the sanitized original stem in the final on-disk filename:
``<uuid>_<safe_stem>.<ext>``. The UUID prefix still guarantees
uniqueness, and downstream consumers (RAG tools, the LLM) can now
recognize the file by its human-readable name regardless of how
the LLM transforms the surrounding path.

`_sanitize_stem()` strips Windows-illegal characters (``<>:"/\|?*``),
control characters, leading/trailing whitespace and dots, and caps
length at 80 chars — keeping the total path well under Windows'
260-char MAX_PATH. Falls back to ``"file"`` if nothing usable
remains after sanitization.

Adds a test case for illegal-char sanitization and updates the
happy-path test to assert the new filename pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full uuid4 (36 chars) is overkill for our use case — we already dedup
blob uploads by SHA-256 content hash before writing, so identical-
content collisions are impossible. The remaining concern is two
concurrent different-content uploads drawing the same random ID,
which at 12 hex chars (48 bits, 2.8e14 possibilities) has a
collision probability of ~1 in 1.4e14 even with 1M concurrent uploads
— effectively zero.

Saves 24 chars off every managed filename, leaving more headroom
under Windows' 260-char MAX_PATH limit. Example:
  before: ce5b8504-5baa-4a8a-89be-fe3b688bd762_my_document.pdf
  after:  9c59c981f8e7_my_document.pdf

Introduces a small `_short_id()` helper so the length is defined in
one place (``MANAGED_ID_LEN``) if we ever need to tune it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-applied by `util/lint.py --all --fix` (step 8: documentation
version consistency check). Aligns the document allowlist across
backend (`utils.py`, `files.py`), frontend
(`UnsupportedFeature.tsx`), and tests so they all reference the
same canonical set of supported extensions and reject formats that
don't have a working extractor.

Bundled into this PR per maintainer request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kovtcharov
kovtcharov marked this pull request as ready for review April 9, 2026 22:07
The fixture patches `gaia.ui.routers.documents.MANAGED_DOCS_DIR` via
its string path, so the symbol no longer needs to be imported at the
top of the test file. Resolves the F401 flake8 error blocking CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kovtcharov
kovtcharov enabled auto-merge April 10, 2026 03:22
@kovtcharov
kovtcharov requested a review from itomek April 10, 2026 03:22
@kovtcharov
kovtcharov added this pull request to the merge queue Apr 10, 2026
Merged via the queue into main with commit d2ffb96 Apr 10, 2026
25 checks passed
@kovtcharov
kovtcharov deleted the kalin/fix-728-document-upload branch April 10, 2026 13:31
@itomek itomek mentioned this pull request Apr 10, 2026
github-merge-queue Bot pushed a commit that referenced this pull request Apr 10, 2026
Release notes for v0.17.2. See
[docs/releases/v0.17.2.mdx](https://github.com/amd/gaia/blob/v0.17.2-release/docs/releases/v0.17.2.mdx)
for the full body.

**Headline features:**
- One-click desktop installers for Windows, macOS, and Linux (#731 +
#743)
- Customizable agents and Builder Agent (#720)
- MCP tool execution visibility in Agent Activity panel (#717)

**Security:** RAG cache integrity verification (#722, closes #447)

**Bug fixes:** drag-and-drop upload in any browser (#729), Linux install
fixes (#743)

**Release & CI:** unified publish workflow (#715), Claude review on fork
PRs (#685), npm OIDC fix (#683), webui package.json sync (#682)

Full changelog: v0.17.1...v0.17.2

---------

Co-authored-by: Tomasz Iniewicz <tomasz@iniewicz.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devops DevOps/infrastructure changes documentation Documentation changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Indexing error: Not found. The file or folder may have been moved or deleted.

2 participants