Skip to content

feat(folders): create a new folder while picking one to open - #881

Merged
xintaofei merged 7 commits into
spacering-net:mainfrom
Jonathan-Asher:pr/new-folder
Oct 9, 2026
Merged

xintaofei merged 7 commits into
spacering-net:mainfrom
Jonathan-Asher:pr/new-folder

Conversation

@Jonathan-Asher

Copy link
Copy Markdown
Contributor

Summary

Adds New folder to the directory browser of the Open Folder dialog, so a project can start from an empty folder without leaving codeg: name the folder, and the browser moves into it, ready for Next. It works the same in the desktop app and in server/web mode, since the browser lists the filesystem of whichever host serves the workspace. The desktop app's native folder picker now also asks the macOS panel for its own New Folder button.

Behavior

  • Browser.
    • A folder-plus button sits beside Home and Up at the start of the path box. It opens a name box at the top of the listing, with a line underneath naming the directory the folder will go in.
    • Enter or the check button creates the folder in the directory being listed. The browser then moves into it, so the path box holds the new folder and Next opens it as the workspace. Creating does not open anything by itself.
    • Escape or the X cancels and leaves the dialog open. The dialog dismisses on Escape from a capture listener on document, which runs before any handler on the input, so the browser claims Escape on window while the name box has focus.
    • A rejected name is explained under the box, which keeps what was typed. Editing the name clears the message. Navigating to another directory closes a half-typed box. While the request runs, the dialog's Next button is disabled.
    • After a folder is created, cached listings are dropped, so going back up shows the new folder.
    • DirectoryBrowser gets an opt-in allowCreateFolder prop. Only the Open Folder dialog's first step turns it on. Its link-targets step and the other pickers that embed the browser (clone destination, worktree path, project boot) are unchanged.
  • Endpoint. create_directory(parentPath, name) is a Tauri command and POST /api/create_directory. Both call create_directory_core, which returns the new folder's path: the parent as given, joined with the name.
  • Validation (CreateDirectoryError, a thiserror enum):
    • The name is trimmed. On every platform it is rejected if it is empty, . or .., or contains /, \, NUL or any other control character. On Windows it is also rejected if it contains < > : " | ? *, ends with a dot (Windows strips a trailing dot without saying so), or is a device name such as CON or nul.txt.
    • The parent must be an absolute path to an existing directory. A symlink to a directory is followed, as the browser follows one when it lists it.
    • Exactly one directory is created (create_dir, never create_dir_all). A missing parent is an error, and nothing above the new folder is created.
    • If an entry of that name already exists (a file, a directory, or a symlink, dangling or not), the request fails with "already exists". The entry is never reused and a symlink is never followed. create_dir also refuses an existing path, so a name taken between the check and the creation gets the same error.
    • Errors map onto the existing codes: invalid name or parent 400, missing parent 404, already exists 409, permission denied 403, any other I/O error 500. Each also carries an i18n key under DirectoryBrowser.newFolder.errors, and the browser renders it with toLocalizedErrorMessage. The new strings are translated in all ten locales.
  • Native picker. openFileDialog passes canCreateDirectories: true for directory picks. tauri-plugin-dialog 2.6 already defaults this to true on macOS, so the call keeps current behavior in place rather than changing it. The Windows and Linux folder dialogs have a new-folder button of their own.
  • Not included.
    • The directory browser has no root or allow-list in server mode. Like list_directory_entries, the endpoint can reach whatever the server process can, and it adds no restriction of its own.
    • No "git init" option. A folder opened without a repository already offers "Initialize Git repository" from the branch chip.

Verification

  • Rust unit tests in commands::create_directory:
    • creating a folder, and the path returned;
    • trimming, and Unicode names;
    • an existing directory, an existing file, and a live or dangling symlink under that name;
    • empty names, ., .., and NUL and other control characters;
    • traversal through the name (../x, ..\x, a/b, absolute names);
    • a missing parent, with nothing created;
    • a relative, empty or file parent;
    • a symlinked parent;
    • a read-only parent;
    • the Windows rules, checked on every host;
    • the mapping to error codes, and every i18n key present in en.json.
  • Vitest:
    • directory-browser.test.tsx:
      • the button appears only when the host asks for it;
      • Enter and the check button create the folder and move into it, and a confirm commits it;
      • the parent shows the new folder when going back up;
      • Escape cancels without closing the dialog;
      • blank names;
      • the localized already-exists and invalid-name errors, and the fallback message;
      • busy reporting, and no second submit while busy;
      • navigating away closes a draft;
      • Arabic.
    • workspace-folder-dialog.test.tsx: creating a folder and opening it with Next, and no New folder button when picking link targets.
    • platform.test.ts: directory picks ask for canCreateDirectories.
    • The existing i18n key-parity and ICU checks.
  • Manual, in WebKit against codeg-server:
    • created a folder and opened it as a workspace;
    • an existing name, a name with a slash, and .. each showed their error;
    • Escape kept the dialog open;
    • the endpoint returned 200, 400, 404, 409 and 401 (without a token) as expected;
    • the Arabic, right-to-left UI.

Starting a project in an empty folder meant leaving codeg to make the
folder first: the Open folder dialog could only pick a directory that
already existed, and in web mode there was no other way to make one on
the server at all.

The dialog's directory browser now has a New folder button beside Home
and Up. It opens a name box at the top of the listing; Enter creates the
folder in the directory being listed, and the browser moves into it, so
Next opens it as the workspace. Escape or the X cancels without closing
the dialog. A rejected name is explained under the box, which keeps what
was typed. DirectoryBrowser takes this as an opt-in allowCreateFolder
prop, so the other pickers that embed it are unchanged.

The folder is made by create_directory_core, shared by the Tauri command
and POST /api/create_directory. It trims the name and rejects empty
names, "." and "..", path separators and control characters (plus the
names and characters Windows reserves, on Windows), needs an absolute,
existing parent, creates exactly one directory, and reports an existing
entry of that name, symlinks included, instead of reusing or following
it. Errors are a thiserror enum mapped onto the existing error codes
(400/404/409/403/500) with i18n keys, translated in all ten locales.

The native folder picker now asks for canCreateDirectories, the macOS
panel's own New Folder button. tauri-plugin-dialog already defaults it to
on; this keeps it on.
xintaofei and others added 5 commits October 8, 2026 22:19
…ated

Escape closed the row even while its request was running, though the X
is disabled then. Closing it cleared the busy flag, so the dialog's Next
came back mid-request; a failure that landed afterwards was dropped
without a word, and a success still moved the browser into the folder
the user had just cancelled. Escape now does what the X does: while the
folder is being created it is still claimed, so the dialog stays open,
but it cancels nothing.

The listener moves into the row, so it reads the IME guard spread on the
name box instead of the path box's.
Windows reads the superscript digits ¹ ² ³ as port numbers, so COM¹ to
COM³ and LPT¹ to LPT³ are device names reserved in every directory, like
COM1 and LPT1. Reject them with the other device names.
Home, Up and Enter in the path box stayed live during the create request.
Moving closed the row, which cleared the busy flag, so the dialog's Next
came back mid-request; a failure that landed afterwards was dropped, and
a success pulled the browser into the new folder over wherever the user
had gone. They now wait for the request, like the row's Escape and X.
Double-clicking a row while a folder was being created opened that row
and moved the dialog on to its links step. That unmounts the browser
without it ever going inactive, so its session guard never moved, and
the create's late answer still reached the host's onValueChange. Back on
the first step, through "Change" or a reopened dialog, it overwrote the
user's new pick, and Next opened the folder made earlier. A navigation
left running the same way had the same gap.

The browser now notes when it unmounts, and a create or a navigation
that settles after that leaves the host alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row's Escape asks its own IME guard, but nothing failed when that
check went: Chromium and WebKit flag the composition on the key event
itself. A composition seen only through compositionstart now has a
test. Two assertions join existing tests: a failure that lands after
Escape hands the host its confirm back, and Home comes back once the
folder is made, as Up already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xintaofei

Copy link
Copy Markdown
Collaborator

codeg work task 287 is done — #881 (4 files, +292/-31).

@xintaofei
xintaofei merged commit b40a90e into spacering-net:main Oct 9, 2026
7 checks passed
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