Skip to content

feat(documents): add a document from a file, in a code editor - #2978

Open
camielvs wants to merge 5 commits into
masterfrom
cvs/document-file-drop
Open

camielvs wants to merge 5 commits into
masterfrom
cvs/document-file-drop

Conversation

@camielvs

@camielvs camielvs commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Description

You can now add a document from a file: drop one onto the Add a document dialog, or click browse. Its name becomes the Title and its text the Content. The Content field is now the shared code editor, so long files scroll and long lines wrap instead of pushing the dialog off the page.

File drop

  • A dashed strip above Title reads "Drop a text file here, or browse", and highlights while a file is dragged over the form.
  • The whole form accepts the drop, so a file that misses the strip by a little doesn't open in the tab.
  • Title is filled only when it's empty or still holds the name from the previous file, so a title you typed is kept. Content is always replaced.
  • Only UTF-8 text up to 1 MB is accepted, checked by decoding the bytes rather than by extension, so extensionless files work. Anything else shows an error under the strip and leaves the fields alone.
  • Text dragged within the form still drops into the fields as usual.

Code editor

  • Content is CodeEditor (Monaco) in a fixed-height box styled like an input, with word wrap and line numbers.
  • It highlights by the title's extension through languageFor, the same rule the document viewers use when a saved document is shown later. That's why a dropped file keeps its extension in the title (train.py, not train), and why the Title placeholder is now model-card.md.
  • CodeEditor takes an optional options prop that overrides its defaults; it's used here for the aria label and placeholder. Other callers are unchanged.
  • Both document dialogs are now sm:max-w-4xl (896px) instead of 512px, to give the editor room.

Shared helpers

  • readTextFile in src/utils/textFile.ts reads a file as strict UTF-8 under an optional byte limit. It returns a reason (too_large, not_text or unreadable) instead of a message, so each caller words its own errors.
  • useFileDrop in src/hooks/ handles dragging and dropping a file onto any element, and reports while a file is over it.
  • The app's other file pickers read files three other ways, and none checks that a file is text or caps its size. Both helpers are general so those pickers can move to them in the follow-up.

Analytics

  • projects.add_document_from_file_completed fires with method: "drop" | "browse". The file name is never sent.
  • Clicks on browse are tracked as projects.add_document_browse_file.click.
image

Related Issue and Pull requests

None.

Type of Change

  • New feature
  • Improvement

Line Breakdown

Category Added Removed
Production code 246 14
Comments 5 2
Tests 419 0
Config / CI / tooling 1 0
Total 671 16

Checklist

  • I have tested this does not break current pipelines / runs functionality
  • I have tested the changes on staging

Test Instructions

  1. Open a project, in Tangent or on its project page, and choose Add → Add a document.
  2. Drop a .md, .yaml or .py file anywhere on the form. Title should become the file name, and Content the file's text, highlighted for that language.
  3. Type a title, then drop a file. Your title should stay.
  4. Drop an image or a PDF. An error should appear under the strip, and the fields should not change.
  5. Drop a file with long lines and a few hundred lines. The editor should scroll, lines should wrap, and the dialog should stay on screen.
  6. Click browse and pick a file. It should fill the fields the same way.

Additional Comments

  • Drag-and-drop was checked in Chromium with simulated drag events, in light and dark themes, at 1280×900 and 640×700. A file dropped onto the editor itself fills it with exactly the file's text, with nothing inserted by Monaco. Pressing Escape while a word suggestion is open closes the suggestion, not the dialog. Dragging a real file from the OS hasn't been tried.
  • Known rough edge, not changed here: in windows 640–928px wide the dialog runs edge to edge, because DialogContent's sm:max-w-* replaces its max-w-[calc(100%-2rem)] margin. Every large dialog in the app already does this.

🤖 Generated with Claude Code

future follow-up: standardize our various file picker workflows into a filepicker component.

@camielvs
camielvs requested a review from a team as a code owner November 2, 2026 20:28
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: cvs/document-file-drop/e1caf41

Comment thread src/components/Project/documentFile.ts Resolved
@camielvs
camielvs force-pushed the cvs/document-file-drop branch from 01cbab4 to 6eed9fc Compare November 2, 2026 21:32
@camielvs
camielvs requested a review from maxy-shpfy November 2, 2026 21:53
@camielvs
camielvs force-pushed the cvs/document-file-drop branch 2 times, most recently from 73ba5f6 to 76e3b2e Compare November 2, 2026 23:45
Comment thread src/components/Project/DocumentForm.tsx Resolved
Comment thread src/components/Project/DocumentForm.tsx Resolved
Comment thread src/components/Project/useFileDrop.ts Resolved
camielvs and others added 5 commits November 3, 2026 16:45
The Add a document dialog had two plain fields, so the only way to add an
existing file was to paste it in. Long content also pushed the dialog off
the page: the textarea grew to fit every line, down and sideways.

A dashed strip above Title now takes a dropped file, or a browsed one. The
whole form accepts the drop, so a near miss doesn't open the file in the
tab. The file's name becomes the title unless one was typed, and its text
becomes the content. Only UTF-8 text up to 1 MB is accepted; anything else
says why and leaves the fields alone.

Content is now the shared code editor in a fixed-height box, with word
wrap, so long files scroll and long lines wrap. It highlights by the
title's extension, the same rule the document viewers use, which is why a
dropped file keeps its extension in the title. Both document dialogs are
wider to give the editor room. CodeEditor takes an options override for
the editor's aria label and placeholder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The app already reads uploaded files four ways, and none checks that a
file is text or caps its size. Only part of the document reader was about
documents, so the general part moves out where the other file pickers can
adopt it.

readTextFile in src/utils/textFile.ts reads a file as strict UTF-8 under an
optional byte limit. It returns a reason rather than a message, so each
caller words its own errors. documentFile.ts keeps what is about
documents: the 1 MB limit, the messages and the title. useFileDrop moves
to src/hooks with its own tests; the form's tests keep only what the form
adds on top.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
readTextFile in src/utils duplicated readTextFromFile in src/utils/dom.ts,
and it and useFileDrop each had one caller. useFileDrop moves back next
to DocumentForm, and textFile.ts goes away.

readTextFromFile becomes the strict reader: it decodes UTF-8 with fatal
errors and refuses NUL bytes, throwing NotTextFileError so the documents
reader can word its own message. The .yaml check that lived inside it
moves to readYamlFromFile, which the two canvas drop handlers now call,
so they still refuse other files. They also gain the text check, and a
file the browser can't read now rejects instead of never settling.

The 1 MB limit stays in documentFile.ts, now checked before reading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each read filled the form when it finished. If an earlier file finished
after a later one, the title stayed the later file's while the content
became the earlier file's. Only the latest read now fills the form, and
Add stays disabled until it finishes, so the draft can't be added while
it is about to change. Local files read instantly, but files kept in a
cloud drive can take seconds to download first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs force-pushed the cvs/document-file-drop branch from a003c45 to e1caf41 Compare November 3, 2026 23:45

This branch has not been deployed

No deployments
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