Skip to content

feat(files): add drag-and-drop and picker upload to the Files panel - #9749

Open
amadsalmon wants to merge 1 commit into
kirodotdev:mainfrom
amadsalmon:feat/files-panel-upload
Open

amadsalmon wants to merge 1 commit into
kirodotdev:mainfrom
amadsalmon:feat/files-panel-upload

Conversation

@amadsalmon

@amadsalmon amadsalmon commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: let a user put a file from their computer into the workspace from the Files panel, by drag-and-drop or a file picker.

The Files panel can move files out of the workspace but not in. Download exists (components.fileRenderers.download, apps.fileExplorer.fileViewer.download); the complete apps.fileExplorer.* key set contains no upload, drop, or new-file string, and the sidebar tree's row menu holds exactly one entry (Add to chat). There is no way to get a file into the workspace from the panel that reads and writes it.

Four other surfaces in the same dashboard already accept a dropped file — the chat composer (components.chatInput.drop_to_attach), Knowledge Base (knowledge.sourcesList.drop_files_here_or_click_to_upload), Design Critique, and the AWS Control file drive (#7983) — so this is a consistency gap on one surface, not a new capability for the product.

Why it matters

For anyone running the dashboard against a remote machine (Cloud Desktop, dev box, pod), the Files panel is the only file tool that already works, because it reads and writes through the gateway rather than the local filesystem. Every alternative for getting a file onto that machine is a separate stack with separate credentials. Kiro IDE's Remote-SSH explorer accepts a dropped file and uploads it into the remote workspace; the dashboard's Files panel looks like that control and sits in the same position, but cannot do that one thing.

Not a goal

What changed (motivation → approach → change)

The Files panel gets an upload path that reuses the composer's gates instead of a second upload stack: drop files anywhere on the panel to upload them into the project root, which the drop overlay names, or pick them with Upload files… on a folder's row menu to upload into that folder. The new endpoint is a sibling of api_upload_file that differs only in its destination, a caller-named directory instead of the chat-attachment scratch dir.

Part What it does
src/kiro_crew/dashboard/file_api/uploads.py (re-exported by the handlers/files.py facade, routed in routes/taskrunner.py) new POST /api/directory-upload?dir=<path>. Same extension allowlist and magic-byte check as api_upload_file; the per-file cap is dashboard.upload_max_mb for every type, video included. Upload never replaces a file: an existing name always answers 409 name_collision.
website/src/api/client/files.ts, website/src/pages/chat/useDirectoryUpload.tsx one hook runs upload and error for both entry points and refreshes every file listing and search an upload can change (the tree, git status, content and filename search, the folder tab) after a batch. While a batch runs the panel shows "Uploading report.md…" (an info StatusNotice, role="status"), then "Uploaded report.md to docs." until dismissed or the next batch; a second drop of a name still uploading into that folder is skipped, not sent. A collision shows "already exists in this folder" in the panel's ErrorNotice.
website/src/pierre/PierreWorkspaceTreeImpl.tsx, tree.tsx, website/src/pages/chat/FileBrowserRail.tsx, website/src/components/ChatDropOverlay.tsx the row-menu action, the drop overlay ("Drop to upload into root"), and the picker. The overlay covers the tree, so a drop never targets a row it hides. Failures show in ErrorNotice.
website/src/i18n/locales/*, docs/feature-map/README.md, docs/system-specs/ 4 new pages.chat.fileBrowserRail.* keys in every locale, the feature-map row, and the learn-cron-dashboard.md and memory-skills-hooks.md specs.

How the write stays safe.

  • The destination is re-gated, not just the folder. The full joined path goes through the same _validate_dashboard_path / is_sensitive_path gate /api/file-write uses, before any body byte is read, so a part named security_policy.json cannot reach the crew home's protected files.
  • Two uploads of the same name cannot both win. The file is created with pinned_fs.write_new_file_pinned, the O_EXCL create the steering, prompt and skill creates already share, so exactly one lands and the other gets name_collision. On Windows, which has no directory descriptor, the same O_EXCL create runs by name while pin_directory holds the folder and its parents.
  • A cloud-files folder is a folder. On Windows the target is pinned with pin_directory(..., links_only=True) (platform_compat_links.py, also used by the rollback in pinned_fs_files.py): a link or junction is refused 403, a OneDrive folder is pinned, any other pin error is 500.
  • Upload never replaces a file. An existing file is never touched, so upload never has to decide who may read a replacement. To replace a file, the user removes or renames it first.
  • A filesystem refusal gets a coded error. An unwritable folder or a full disk answers JSON 500 with code: "upload_failed", never a bare text 500.
  • The write is ancestor-pinned and leaves nothing behind on failure. The folder chain is pinned against a symlink or junction swap after validation, and a symlink, junction or device at the destination name is refused. The bytes are written under the final name, so a reader can see a partly written file while the upload runs; a failed write removes it, and only while the name still holds the file this upload created.

What upload refuses on purpose.

  • Types the chat composer refuses. The upload admits exactly what api_upload_file admits, the same extension allowlist and magic-byte check, so a file with no extension (Makefile, Dockerfile) or an unlisted one (.toml, .sql) answers unsupported_file_type. The workspace is where agents read and run files, and Define the Files-panel upload allowlist scope (extensionless workspace files are refused today) #13038 is deciding what the Files panel should admit there; until then this route admits nothing an existing upload route does not.
  • Names a host cannot hold. An empty name, ., .. or a name with a control character answers invalid_filename everywhere. On a Windows gateway, so does a name Windows cannot store (<>:"\|?*, a trailing dot or space, a device name such as CON), before any write; a Linux or macOS gateway keeps such a name as typed, as every other tool writing into the workspace does.

Backwards compatibility

Compatible: a new endpoint, new i18n keys and new optional props on the tree and rail components.

Removes nothing: no route, key, keyword or behavior main has is removed or narrowed.

Tests

  • test/test_handlers_files_directory_upload.py: successful write; an existing name answering 409 name_collision with the original unchanged, including for a request that sends overwrite=1; the allowlist, size, magic-byte, empty-payload, invalid and sensitive-destination refusals; a path-traversal multipart name; a concurrent same-name create landing only the winner; a fresh file's mode matching what a new file in that folder gets; EACCES/ENOSPC answering 500 upload_failed, a write that fails halfway leaving no file, on both the pinned and the Windows branch; and on the Windows branch, a failed first identity probe still rolling back the empty file so a retry is not refused; on the Windows branch, a cloud-files folder accepting an upload and rolling back a failed one, a link refused 403, and an unrelated pin error answering 500. test/test_platform_compat_filesystem.py covers pin_directory's links_only (default still refuses a cloud-files folder; links_only pins it and still refuses a junction or non-directory).
  • Frontend: useDirectoryUpload (a successful batch refreshes every listing and search query that can gain the new file, including keys built from the backend's realpath; a collision shows the exists notice and never retries; errors; a later success clearing an earlier failure notice; the in-flight and success lines, their clearing on dismiss, next batch and project change; a second drop of a name still uploading sends no request), PierreWorkspaceTreeImpl.contextMenu.test.tsx (the menu item on folder rows only) and FileBrowserRail.test.tsx (the overlay names the root; a drop released over a folder row or a file row uploads into the root; the picker). FileBrowserRail.test.tsx and useDirectoryUpload.test.tsx pass 3 repeats and 3 shuffled orders (--sequence.shuffle, seeds 11, 22, 33).

Manual verification

Verified against an isolated gateway from this worktree using the offline harness (spawn_feature_gateway + the packaged fake ACP backend — no model, no credentials, no network), driving the real UI. The row-menu and post-upload screenshots below are from that run; the drop-overlay, name-collision and upload-failure frames are gateway-free captures of the real FileBrowserRail with /api/** stubbed, /api/directory-upload answering the backend's real refusal shapes.

Screenshots / video

Drop overlay active, naming the project root a drop uploads into (real FileBrowserRail, light theme), then the directory row menu with the new action, and the tree after a successful upload:

drop overlay: Drop to upload into kiro-notes

directory row context menu with Upload files…

Post-upload tree

tree showing the uploaded file in the target directory

While an upload runs, and after it finishes (real FileBrowserRail, light theme, through the docs row menu's Upload files…, /api/directory-upload held pending, then answering 200):

info status: Uploading report.md…

info status: Uploaded report.md to docs., with a dismiss button

A name collision (409 name_collision): the panel names the file and says what to do. Real FileBrowserRail, light theme, triggered through the row menu's Upload files… with the backend's 409 stubbed:

name collision notice: already exists in this folder, rename or remove it, then upload again

The upload-failure state (ErrorNotice above the tree, naming the file and the backend refusal, with dismiss and agent hand-off):

upload failure ErrorNotice naming the refused file above the tree

The collision frame is light mode; the other four are dark mode (the fixture's default theme).

Related Issues

Fixes #9666

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (docs/system-specs/modules/learn-cron-dashboard.md, memory-skills-hooks.md)
  • No secrets, credentials, or internal references in the diff

@amadsalmon
amadsalmon requested review from a team and chenmingwei23 September 9, 2026 17:03
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 9, 2026
@amadsalmon
amadsalmon force-pushed the feat/files-panel-upload branch from cf10b5f to 7884f55 Compare September 9, 2026 22:04
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5, fork) — ✅ PASS

Design-level review of 07b80f55bba0d5ec435ae12e1fe3ba8f72adcf15 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

Design-Verdict: PASS

No design-level concerns. The new create-only endpoint reuses the composer's admission gates and the existing path gate, re-checks the full destination, and changes no default behavior of existing helpers.

Notes

  • The 2,000-line cap: uploads.py grows by about 570 lines. Run scripts/check_file_line_cap.py to confirm it stays under 1,600, or move api_directory_upload into its own sibling module, as "a new responsibility gets a new module" asks.
  • upload_failed covers both a Windows pin refusal (a sharing violation) and a full disk or unwritable folder. Retrying helps with the first but not the other two, so the frontend can't tell the user whether to try again. That's reversible and can be a follow-up.

[DESIGN-REVIEWED] 07b80f5

Description read

[DESCRIPTION-READ] 9e5c8b8fffdaaaa4c55855230052a5cdb12fa44e936182fd8d247f149dfd0f07

That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5, fork) — ✅ PASS

UX-level review of 07b80f55bba0d5ec435ae12e1fe3ba8f72adcf15 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

UX-Verdict: PASS

Nothing blocks. The screenshots show every new control and state (row-menu "Upload files…", drop overlay, progress, success, collision error, refused-type error), but this fork lane has no blind read, so no first-time reader has judged them.

Notes

  • The refused-type error ("Couldn't upload setup-tool.exe: Unsupported file type: .exe", attachment-03) says nothing about what to do next. Common workspace files like .toml, .sql and Makefile hit the same dead end. Add a short line saying which file types this panel accepts.
  • "Uploaded report.md to docs." shows while docs is still collapsed (attachment-07), so the new file isn't visible. Make the notice reveal the uploaded file in the tree, or add a link that does.
  • upload_failed appends the raw server result.error after a colon. Map the known codes (invalid_filename, upload_failed, size cap) to plain sentences instead of passing the server text through.

[UX-REVIEWED] 07b80f5

Description read

[DESCRIPTION-READ] c6b32195322b59e014d160f95a30162e7868e1bd2737bf6c4905ee736d17534d

That sha256 names everything this verdict read: the PR description, plus the 10 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5, fork) — ✅ PASS

Premise-level review of 07b80f55bba0d5ec435ae12e1fe3ba8f72adcf15 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

First-Principles-Verdict: PASS

Additive capability beside existing ones; no default moved, no existing control relocated. One fail-closed branch the author calls unreachable.

Notes

  • Item 7 — oversized: the docstring calls the ancestor_pin_unsupported 501 branch "unreachable in the current CI matrix: every one of Linux/macOS/Windows takes one branch or the other". Nobody hits it, yet it adds a response code that must be kept up. Subtraction: delete the ancestor_pin_unsupported outcome and its 501 response, and leave the two reachable branches.

What this change ships

Inventory (8 items) — 7 justified

Intent: let a user put a file from their computer into the workspace from the Files panel. ADDITION.

  1. Dropping an OS file anywhere on the Files rail uploads it into the project root, and the overlay names that root — justified
  2. A folder row's menu gains Upload files…, which uploads into that folder — justified
  3. New POST /api/directory-upload, which reuses the composer's extension allowlist, magic-byte check and owner gate — justified
  4. An existing name is never replaced: it answers 409 "already exists in this folder" — justified
  5. The rail shows "Uploading…" and "Uploaded … to …" status lines and an ErrorNotice when an upload fails — justified
  6. On Windows, pin_directory / unlink_verified_by_name gain links_only, so a OneDrive folder accepts uploads — justified
  7. A 501 ancestor_pin_unsupported refusal for platforms that have neither pin primitive — oversized: unreachable on every supported host
  8. Four UX-evidence screenshots under temp-screenshots/ — justified

[FIRST-PRINCIPLES-REVIEWED] 07b80f5

Description read

[DESCRIPTION-READ] 01db23aa6f51d8c9950045054d6e8233b3647f902321b444272308927fa058b5

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review (fork) — ✅ no blocking findings

Reviewed 07b80f55bba0d5ec435ae12e1fe3ba8f72adcf15 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 07b80f5

@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review (fork) — ✅ no blocking findings

Reviewed 07b80f55bba0d5ec435ae12e1fe3ba8f72adcf15 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 07b80f5

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 9, 2026
@amadsalmon
amadsalmon force-pushed the feat/files-panel-upload branch 2 times, most recently from 97e81e3 to ea7d9f9 Compare September 10, 2026 12:26
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 10, 2026
@amadsalmon
amadsalmon force-pushed the feat/files-panel-upload branch from 83feb68 to c7e055c Compare September 10, 2026 13:38
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 10, 2026
@amadsalmon
amadsalmon force-pushed the feat/files-panel-upload branch from c7e055c to febd887 Compare September 10, 2026 15:08
@amadsalmon

Copy link
Copy Markdown
Contributor Author

Item 2 of #12124; which types are admitted at all is #13038.

@amadsalmon

Copy link
Copy Markdown
Contributor Author

Same item as spans aa83fa5b4902 and 8e181b1ca965: a root-aimed picker goes with the other #12124 polish.

@amadsalmon

Copy link
Copy Markdown
Contributor Author

Item 2 of #12124.

@iamwhatever

Copy link
Copy Markdown
Contributor

Holding at aa2b06ecc9a3c67af751ef63fd60c387b245af03. No blocking defect found in what I read, and CI is green on this head. These items keep it out of approval:

— crew-pr-reviewer (r116)

@amadsalmon

Copy link
Copy Markdown
Contributor Author
  • Comment over the destination check says the Windows branch runs without ancestor pinning span=57f2e15af10b — fixed in e15682f

The comment now says POSIX stats through the pinned dir_fd, and Windows checks by name under the pin_directory handle held across the check and the create, which per pin_directory's contract blocks renaming that directory or any above it. Comment-only change; flake8 and the upload tests pass.

@amadsalmon

Copy link
Copy Markdown
Contributor Author

On the hold at aa2b06ecc9 (crew-pr-reviewer r116), item by item. New head: e15682f077, the same change plus the comment fix below, rebased onto main 0b89e0c6f7.

Sent by an AI agent on behalf of Amad

@amadsalmon

Copy link
Copy Markdown
Contributor Author
  • The opaque drop overlay hides the folder row a drop targets span=92c0c9c6047e — fixed in 3a209e9

A drop anywhere on the Files rail now uploads into the project root, and the overlay names it ("Drop to upload into "), so no hidden row chooses the destination. resolveUploadTargetDir and its tests are removed; a specific folder is reached through its row menu's Upload files…, which the picker already covers. FileBrowserRail.test.tsx pins a drop over a folder row and over a file row both going to the root.

@amadsalmon

Copy link
Copy Markdown
Contributor Author
  • Refresh content-search results after uploading span=70a05c0274cf — fixed in 3a209e9

useDirectoryUpload's invalidate now also invalidates ['file-grep', projectDir] after a successful batch, beside project-tree and git-status. useDirectoryUpload.test.tsx asserts all three prefixes and fails without the line.

@amadsalmon

Copy link
Copy Markdown
Contributor Author

@bolichen97 @chenmingwei23 this is ready for review. CI is fully green on 3a209e9ba9 (readiness: passed) and every AI review lane passes.

@bolichen97, the owner gate from your 2026-09-25 review is in place: api_directory_upload calls require_owner_dashboard_request before it parses the query, probes a path or reads the body, and test_directory_upload_refuses_a_non_owner covers it. Could you take another look and approve, or dismiss the change request?

Thanks!

Sent by an AI agent on behalf of Amad

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the update. I re-reviewed head 3a209e9ba9.

My earlier change request is resolved. api_directory_upload now calls require_owner_dashboard_request(request, "upload.directory") as its first statement, before the query parse, the directory probe and the multipart read. That is the same order api_file_write uses on main, and test_directory_upload_refuses_a_non_owner covers it. I found no new blocking issue in the handler: the full destination is re-gated through _validate_dashboard_path, the create is O_EXCL so an existing name always answers 409 name_collision, and the extension allowlist and magic-byte check match api_upload_file.

One thing blocks the merge: the branch conflicts with main. The only conflicting file is docs/feature-map/README.md; the locale files and learn-cron-dashboard.md merge cleanly. Please rebase onto current main, resolve that row, and push. When the new head is green with readiness: passed, I will take a final look and replace this change request.

What I checked on this head: PR Readiness is green, every AI review lane passes, the linked issue is a T2, and the owner gate and its test are present.

@amadsalmon

Copy link
Copy Markdown
Contributor Author
  • Refresh filename-search results after uploads span=70a05c0274cf — fixed in 5cbfb99

Fixed for the whole class: a successful batch now invalidates every listing or search that can gain the new file. Beside project-tree, git-status and file-grep (keyed by projectDir), it invalidates the prefixes rail-name-search, browse-files and folder-file-search, whose keys use the backend's realpath or the folder tab's own folder, which the hook cannot know. useDirectoryUpload.test.tsx seeds each key with its reader's root and fails without the fix.

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed the change since 335d208 at 5cbfb99. Besides the rebase onto main, the only change is in useDirectoryUpload: a successful batch now also invalidates the rail filename search, the folder tab listing and the folder tab search, by key prefix because those are keyed by roots the hook cannot know. I checked that the three keys match the ones FileBrowserRail.tsx and FolderPanel.tsx read on main, and the updated test seeds each key and asserts it is invalidated. No new findings.

This stays a comment, not an approval: the PR adds a new write route and edits docs/feature-map/README.md, so the approval belongs to a human maintainer, and readiness and the test jobs are still running on this head.

@iamwhatever

Copy link
Copy Markdown
Contributor

HOLD at 88da04a for maintainer review.

The items from our r116 hold are addressed: the Opus comment wording is fixed, @bolichen97's change request is dismissed, and the root-target upload button and accepted-types message are now items 9 and 10 of #12124. All AI lanes pass and every check is green on this head. I found no defect in what I read: api_directory_upload runs the owner gate before any parse, probe or body read, refuses sensitive destinations, and creates with O_EXCL under a pinned directory handle.

What still needs a person:

  1. New write route. POST /api/directory-upload (taskrunner.py#L62) writes caller-named files into the workspace, so a maintainer has to review it and its auth checks.
  2. Files-panel upload UX polish: visible drop targeting, translated failure text, destination in Replace dialog #12124 triage. It has no assignee and its Due: 2026-10-17 has passed. Someone with triage rights needs to set an owner and a current due date.
  3. needs-author-decision. The label has no recorded question. A maintainer should state the decision or remove it.

I read the route, the handler and the lane bodies, not every test file. I did not run tests.

— crew-pr-reviewer (r119)

@bolichen97

Copy link
Copy Markdown
Contributor

Human design review is required before this PR can merge. Workspace upload adds a new remote write path. A maintainer must approve destination scope, file types, collision behavior, and replacement rules. This comment records the design gate only; it is not a code defect.

@amadsalmon

Copy link
Copy Markdown
Contributor Author

@bolichen97 thanks, understood: this needs a maintainer's design sign-off. To make that review quick, here are the four rules as the PR implements them today:

  • Destination scope: the project directory only. A drop goes to the project root; a folder's Upload files… menu goes to that folder. The full destination path goes through the same _validate_dashboard_path / is_sensitive_path check as /api/file-write, so sensitive paths are refused. Owner-only, like the other write routes.
  • File types: exactly what the chat composer's upload (api_upload_file) accepts: the same extension allowlist and magic-byte check, with the per-file cap from dashboard.upload_max_mb. Widening that list for the workspace is left to Define the Files-panel upload allowlist scope (extensionless workspace files are refused today) #13038.
  • Collisions: an existing name is always refused with 409 name_collision, and the original is left untouched. Two uploads with the same name can't both succeed (O_EXCL create).
  • Replacement: none. Upload never overwrites. To replace a file, the user removes or renames it first.

Whoever reviews it: if you'd rather any of these be different, tell me which one and I'll change it.

@amadsalmon

Copy link
Copy Markdown
Contributor Author
  • No pending state for an upload in flight — fixed in bd6a8cc

While a batch runs the rail shows an info status ("Uploading report.md…" / "Uploading 3 files…", role="status") in the notice slot, then "Uploaded report.md to docs." until dismissed or the next batch. A second drop of a name still uploading into that folder is skipped and never sent, so it cannot hit the 409 against its own first attempt. Both lines are in every locale, with plural forms.
Tests in useDirectoryUpload.test.tsx and FileBrowserRail.test.tsx cover the progress line, the success line naming file and folder, clearing on dismiss / next batch / project change, and the skipped duplicate drop; 10 of the 11 fail without the change. Frames upload-progress.png and upload-success.png are in the description.

— Sent by an AI Agent on behalf of Amad

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed at bd6a8cc.

What changed since 818556e: frontend only. The Files rail now shows an "Uploading ..." status while a batch runs and a dismissible "Uploaded ... to ..." line after it, a second drop of a name still uploading into the same folder is skipped instead of sent, and the new strings are in every locale with plural keys registered. The backend route is unchanged.

The delta reads correctly to me. The in-flight set is keyed by the normalized folder plus name, keys are released when the batch ends, a project switch clears the notices, and StatusNotice keeps both lines as polite role="status" updates. The two new tests cover the progress line, the success line and the skipped re-drop.

One item on unchanged backend code, inline below: the GPT lane now blocks on the Windows create path in _write, and the Design and First Principles lanes point at the same spot. Routing that branch through one pinned_fs by-name create would clear the block and remove the duplicate sequence in one change.

Status: this still needs a maintainer's design sign-off on the new write route (destination scope, file types, create-only collision rule), so I am leaving a comment rather than an approval.

Checked: interdiff 818556e to bd6a8cc, the Windows branch of _write against write_new_file_pinned at this head, CI and lane results on this head. Not run locally.

os.close(fd)
except OSError:
pass
if created is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Windows rollback skips the second identity probe

The Windows branch runs its own create, write and rollback instead of the pinned_fs sequence. When os.fstat(fd) fails, created stays None, so the rollback only closes the descriptor and the empty file stays under the O_EXCL name. The upload answers 500, and every retry of that name then answers 409 until the user deletes the empty file by hand. write_new_file_pinned handles this case by asking the descriptor for its identity a second time before closing it.

if created is not None:
    pinned_fs.unlink_verified_by_name(
        Path(target_dir), safe_name, (created.st_dev, created.st_ino)
    )

Suggestion: add a by-name variant of the pinned_fs create (same steps, os.open(dest, ...) instead of dir_fd) and call it here, so both platforms share one sequence. That clears the GPT lane block on this line and the duplicate-code note from the Design and First Principles lanes. If that is too big for this PR, at least retry os.fstat(fd) before the close when created is None.

Rule: one owner for each filesystem transaction; a rollback must not leave behind a name that the request created.

@amadsalmon

Copy link
Copy Markdown
Contributor Author
  • Failed Windows metadata probe strands an empty upload — fixed in f1767ed

The by-name (Windows) rollback now re-asks the identity through the still-open descriptor when the first os.fstat failed, then removes the name only while it still holds that inode, the same contract as write_new_file_pinned (pinned_fs_files.py). With both probes failed the name stays, as there.
New test test_a_failed_first_identity_probe_still_rolls_back_on_the_by_name_branch fails the first probe with EIO: the empty file is removed and a retry of the same name returns 200. It fails without the change; the file passes 3 repeats.

— Sent by an AI Agent on behalf of Amad

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review at f1767ed.

Delta since bd6a8cc: one backend change in dashboard/file_api/uploads.py plus one regression test in test/test_handlers_files_directory_upload.py. Everything else in the PR is unchanged; the locale files differ only by what main added.

Previous finding (Windows rollback after a failed first os.fstat): resolved. On the by-name branch the rollback now re-asks the identity through the still-open descriptor when the first probe failed, closes, and unlinks only through unlink_verified_by_name with that identity. This is step for step the rollback contract of pinned_fs.write_new_file_pinned, including leaving the name in place when both probes fail (nothing was written by then, so only an empty file can remain). The new test forces the first probe to fail on the by-name path, checks the 500 and that the file is gone, and checks that a retry of the same name returns 200 with the right bytes.

What I verified: read the delta at this head, compared the new rollback with write_new_file_pinned on main, and read the new test. I did not run the suite locally. The AI review lanes have not run on this head yet, so the earlier GPT block still points at the old head.

Optional, not blocking: write_new_file_pinned needs a dir_fd, so the by-name copy is still a second spelling of the same sequence. A by-name mode in pinned_fs would let a future fix land once. Fine to leave for a follow-up.

No new findings. This is a feature that still needs a maintainer to agree to the design (destination scope, allowed file types, create-only collision rule), so this is a comment, not an approval.

@amadsalmon

Copy link
Copy Markdown
Contributor Author
  • Directory upload refuses every OneDrive / cloud-files folder on Windows — fixed in 0a69a16

pin_directory gains links_only (default unchanged for every other caller), using the same _win_reparse_refused rule as open_file_no_reparse. The upload's Windows branch pins with links_only=True, so a cloud-files folder is pinned and only a link or junction (NotADirectoryError / ELOOP) or a final-path mismatch answers 403 symlink_refused; any other OSError now reaches the 500 upload_failed handler. The rollback's unlink_verified_by_name pins by the same rule, so a failed write in a cloud-files folder is still cleaned up. The final-path comparison is unchanged.
Tests: TestPinDirectoryLinksOnly (default still refuses a cloud-files dir; links_only pins it and still refuses a junction and a non-directory), and handler tests for a cloud-files upload landing, a PermissionError answering 500, and a cloud-files rollback; the handler tests fail without the change.

— Sent by an AI Agent on behalf of Amad

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review at 0a69a16 (previous review was on f1767ed).

What changed since f1767ed (the base also moved to current main, which did not touch any file in this PR):

  • Windows upload now pins the target with pin_directory(target_dir, links_only=True), so a OneDrive or other cloud-files folder (a reparse directory whose tag is not a name surrogate) is pinned instead of refused with 403. A link or junction at the name is still refused. This addresses the blocking Opus finding on f1767ed.
  • pin_directory gains a keyword-only links_only (default False, so every other caller is unchanged). It reuses the existing _win_reparse_refused / win_fd_is_link helpers that open_file_no_reparse already uses, so a tag that cannot be read still fails closed as a link.
  • unlink_verified_by_name forwards links_only, and the upload rollback passes it, so the rollback pin and the create pin agree on the same folder.
  • A pin refused for a reason other than a link (sharing violation, EACCES) now propagates and answers 500 upload_failed instead of 403 symlink_refused. Only NotADirectoryError and ELOOP still map to symlink_refused. The final path check after the pin is unchanged.
  • New tests: the four pin_directory(links_only=...) cases on the stubbed Windows branch (default refuses cloud-files, links_only pins it, a junction and a file stay refused), plus handler tests for a cloud-files upload landing, a non-link pin refusal answering 500, and rollback in a cloud-files folder.

Verified: read the full delta between the two heads; confirmed main did not move any touched file between the two bases; ran test/test_handlers_files_directory_upload.py and test/test_platform_compat_filesystem.py at this head on Linux: 106 passed, 5 skipped. The Windows branch is exercised only through stubs here; no Windows host was run.

Findings: none new. The previous review's note still stands as an optional follow-up: the Windows create sequence is a second spelling of write_new_file_pinned, now behavior-equivalent including the cloud-files case.

The AI review lanes are still running on this head, so their verdicts are not yet known. This is a feature that still needs a maintainer design sign-off on destination scope, allowed file types and the create-only collision rule, so this is a comment, not an approval.

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review at 1ff3664.

Delta since 0a69a16: a rebase onto current main plus one test edit. test_windows_failed_write_into_a_cloud_files_folder_still_rolls_back now patches pin_directory through the kiro_crew.pinned_fs facade instead of importing pinned_fs_files directly. The facade forwards a patch to every owner module that holds the name, so the rollback in unlink_verified_by_name still sees the stub, and the test still asserts the rollback pin used links_only. No production code changed. The rebase hunks in the locale files match the previous head.

What I verified: the range diff between the two heads, that the facade forwards writes to pinned_fs_files, and test/test_handlers_files_directory_upload.py at this head against this head's source on Linux (59 passed). Windows paths are stubbed in that file, not run on Windows.

No new findings. The AI review lanes are still running on this head.

This is a comment, not an approval. The feature still needs a maintainer to sign off the design: the destination scope, the allowed file types and the create-only rule for name collisions.

Add POST /api/directory-upload so the Files panel's drag-and-drop, drop
overlay and "Upload files..." action can write a file into a directory the
panel already shows. Reuses api_upload_file's admission gates (extension
allowlist, size cap, magic-byte content check) and the dashboard path gate,
with platform-aware ancestor pinning.

Upload is create-only. On POSIX the file is created through
pinned_fs.write_new_file_pinned, the shared O_EXCL create under the
pinned directory with an identity-checked rollback; on Windows the same
O_EXCL create runs by name under the held pin_directory handle. An
existing name is never replaced: a name collision, including one lost to a
concurrent upload, answers 409 name_collision, and the panel shows "<name>
already exists in this folder. Rename your file, or remove the existing
one, then upload again." A
failed write removes the partly written file and answers 500
upload_failed. A new file gets atomic_write's default mode.

The handler opens with the dashboard owner gate
(require_owner_dashboard_request), matching the sibling /api/file-write
route: a non-owner holding a channel-created dashboard session cannot use
it to plant steering docs, SKILL.md or the MCP config. The gate runs before
the query parse, path probe and multipart read, so a non-owner also cannot
learn whether a path exists.

Tests: non-owner refused (403 owner_only) before the probe; owner positive
control; an existing name answers 409 name_collision with the original
bytes unchanged, including for a request that sends overwrite=1; a rival
created just before the create wins and the upload gets 409; a write that
fails mid-body leaves no file on both the pinned and the by-name branch.

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

fork Pull request from a fork (external contributor) needs-author-decision PR blocked on author input readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Files panel: add upload to the file tree

6 participants