creating a file just creates it — and an empty file is editable

Two halves of one behaviour.

── New file no longer navigates ──

It created the file and then took you to /code-editor, which made it the only
action in the browser that leaves the browser. Creating a folder does not leave
the folder; uploading does not open what you uploaded. Now the file appears in
the listing and that is the whole operation.

Editing is the same double-click that opens anything else — the viewer's pane
already has an editor in it, with a save. One way to open a thing, one place it
appears.

── The bug that would have broken that on the first try ──

FileViewerHeader gated its Edit toggle on `editable && content`. `content` is
`string | null` and an empty file is the EMPTY STRING, which is falsy — so a file
with nothing in it rendered no pencil and could not be edited. That is precisely
the file you most want to edit, and it is exactly what "New file" produces: the
flow above would have dead-ended on its first use.

Now `content !== null`, which is what FileViewerBody already used two files away.

Checked the other two truthiness gates rather than changing them blindly, and both
are right: `isJson` returns false for empty (an empty string is not JSON, and
JSON.parse would throw), and Read Aloud stays hidden because speaking an empty
file is not a thing.

`Open in editor` still goes to /code-editor and is untouched — that route is a
different tool, with a tree and tabs, and today's links are the first way to reach
it at all. Worth deciding separately.

tsgo clean, frontend builds, 817 pass / 7 fail unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 20:08:43 +00:00
co-authored by Claude Opus 5
parent d7b602ff34
commit c945f50e60
2 changed files with 38 additions and 8 deletions
@@ -325,11 +325,16 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
};
/**
* Create an empty file and open it in the editor.
* Create an empty file. That is all it does.
*
* It used to navigate straight into `/code-editor`, which made "New file" the one action in the browser
* that took you somewhere else. Creating a folder does not leave the folder; uploading does not open
* what you uploaded. Now this behaves like both: the file appears in the listing, and editing it is the
* same double-click that opens anything else — the viewer's pane has an editor in it.
*
* The name is taken whole, extension and all — `notes.txt`, `deploy.sh`, `.env`, `Makefile` — because
* the extension is what the editor uses to pick a language, and guessing one for the user would be
* wrong more often than not. No extension is fine too; that file simply opens as plain text.
* the extension is what picks the language, and guessing one for the user would be wrong more often
* than not. No extension is fine; that file opens as plain text.
*
* Refuses to clobber. `/write` is an overwrite, so creating over an existing name would silently empty
* it — the one outcome nobody wants from a menu item called "New file". The check is a `read` because
@@ -348,7 +353,6 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
await files.writeFile(filePath, '');
await refresh();
toast.success(`Created "${name}"`);
navigate(`/code-editor?${EDITOR_FILE_PARAM}=${encodeURIComponent(filePath)}`);
} catch {
toast.error('Failed to create file');
}
@@ -1,4 +1,19 @@
import { Download, Loader2, Music, Film, Image, FileType2, Volume2, ScanText, FileText, AudioLines, FolderArchive, Save, Pencil, Eye } from 'lucide-react';
import {
Download,
Loader2,
Music,
Film,
Image,
FileType2,
Volume2,
ScanText,
FileText,
AudioLines,
FolderArchive,
Save,
Pencil,
Eye,
} from 'lucide-react';
import { getIcon } from 'material-file-icons';
import { getLang } from './file-types';
import { useFileViewer } from './FileViewerContext';
@@ -49,7 +64,10 @@ export const FileViewerHeader = () => {
<span className="text-[10px] font-mono uppercase tracking-wider shrink-0 opacity-60">
{fileType === 'code' ? getLang(fileName) : fileType}
</span>
{editable && content && (
{/* `content !== null`, NOT `content` — an empty file is the empty string, which is falsy, so a
file with nothing in it had no way to start editing. That is precisely the file you most want
to edit, and it is what "New file" produces. The body already got this right. */}
{editable && content !== null && (
<button
onClick={() => setEditing(!editing)}
className="p-1 rounded hover:bg-current/10 transition-colors cursor-pointer"
@@ -95,7 +113,11 @@ export const FileViewerHeader = () => {
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
title="Extract Audio"
>
{extractAudioLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <AudioLines className="h-3.5 w-3.5" />}
{extractAudioLoading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<AudioLines className="h-3.5 w-3.5" />
)}
</button>
)}
{fileType === 'archive' && (
@@ -105,7 +127,11 @@ export const FileViewerHeader = () => {
className="p-1 rounded hover:bg-current/10 disabled:opacity-40 transition-colors cursor-pointer"
title="Extract"
>
{extractLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <FolderArchive className="h-3.5 w-3.5" />}
{extractLoading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<FolderArchive className="h-3.5 w-3.5" />
)}
</button>
)}
{handleSaveResult && (