Repository navigation
How New Files and Unsaved Changes Work
This chapter explains Untitled tabs (File > New File, Cmd+N) and Remember unsaved changes, the hot exit that keeps unsaved text across restarts. The user side is in New File and Unsaved Changes; tabs in general are in How the editor works.
Many editors let you type into a new tab without picking a file first, and never lose that text or unsaved edits when you quit. Before this, every tab was a file, and closing the window asked you to throw unsaved edits away (Cmd+Q did not even ask). People use the editor as a scratch pad, so both gaps hurt.
Two parts work together: a pseudo tab for text without a file, and a small store that copies unsaved text to disk.
An Untitled tab is a pseudo tab (see pseudoTabs.ts), like terminal and commit tabs. Its path is untitled: plus a short id from the time and a random suffix (newUntitledPath in src/lib/stores/untitledTabs.ts), so it never looks like a file and never clashes across windows or restarts. Being a pseudo tab, it is skipped by the tab limit, Unload hidden tabs, the Files panel and Back / Forward without any special code.
EditorGroupContent.svelte renders UntitledView.svelte for it: a CodeMirror editor with the base extensions and word wrap, no Git features. It registers with fileCommands like FileView, so File > Save, Save All, Revert and the MCP text tools reach it. The tab is dirty while it has text. The tab strip shows unsavedText.title(path), the first line with text (untitledTitle).
Saving opens the native save dialog (plugin-dialog), suggesting suggestedFileName(text). The text is written with the existing write_worktree_file command: inside a workspace folder relative to that folder, outside it relative to the chosen file's parent. Inside, repoStore.replaceUntitledTab swaps the tab's path for the file's (replaceTabPath in tabs.ts), so a FileView takes its place and reads the file. Outside, the tab closes.
sequenceDiagram
participant Editor as FileView / UntitledView
participant Store as unsavedText
participant Rust as unsaved.rs
participant Repo as repoStore
Editor->>Store: schedule(tabPath, text) on each edit
Store->>Rust: unsaved_write after 500 ms (raw body)
Note over Repo: window closes or workspace closes
Repo->>Store: keepUnsaved: flush every waiting write
Repo->>Repo: saveTabSession, settings.flushNow
Note over Repo: next time the workspace opens
Repo->>Store: open(workspaceId)
Store->>Rust: unsaved_list
Repo->>Repo: sessionWithKept, restore tabs as dirty, not dormant
Editor->>Store: read(tabPath), start from the kept text
-
Writing. On every edit that leaves a tab dirty, the editor calls
unsavedText.schedule. After 500 ms the text goes tounsaved_writeas a raw body (one JSON line, then the text), the same formatline_change_marksuses, so large texts cross the bridge without JSON escaping. -
Forgetting.
repoStore.setDirty(path, false)(save, revert, editing back to the saved text) and closing a tab by hand callunsavedText.discard. Writes and removals of one tab run in a promise chain, so a slow write never brings back text that was just saved. -
Leaving.
confirmCloseWindowandconfirmDiscardAllgo throughconfirmLeave. With the setting on,keepUnsavedwrites everything waiting (and any dirty tab never written, for edits made before the setting was on), saves the tab session and flushes settings, then lets the window or workspace go without asking. Clear Cache callskeepUnsavedtoo. Cmd+Q ends the app without asking the page, which is why writes happen during typing and not only at the end. -
Restoring.
restoreTabsasksunsavedText.open(workspace.id)for the kept tabs: Untitled tabs filed under this workspace (or listed in its saved session) and files inside its folders.keptTabsturns a file that no longer exists into a new Untitled tab with its text (moveToUntitled).sessionWithKept(pure, intabSession.ts) merges them into the saved session; with Reopen tabs on start off, only kept tabs stay. Kept tabs come back dirty and not dormant, so Save All reaches them.FileEditor.load(fileEditor.svelte.ts) reads the kept text and starts the editor from it, with the file on disk as the baseline, so the tab is dirty against the real file. -
The workspace id. Adding or removing a folder, or Save Workspace As, changes the workspace id;
workspaceIdChangedrewrites the open Untitled tabs under the new id.
src-tauri/src/unsaved.rs keeps one file per tab in ~/.nibtree/unsaved/, named by the SHA-1 of the tab path (git2::Oid::hash_object, as Local History does). Each file is the raw body as it arrived, with savedAt added: a JSON line, a newline, the text. list reads only the first line of each file. Writes go through a temporary file and a rename. Paths must be absolute or untitled:<id>; texts over 16 MB or not UTF-8 are refused.
| File | What it does |
|---|---|
src/lib/stores/untitledTabs.ts |
Untitled paths, titles and suggested file names (pure) |
src/lib/stores/unsavedText.svelte.ts |
Scheduling, ordered writes, flush, titles, parked text for moved tabs |
src/lib/views/files/UntitledView.svelte |
The Untitled editor and Save |
src/lib/views/files/fileEditor.svelte.ts |
Starts from kept text, schedules writes |
src/lib/stores/repo.svelte.ts |
newUntitledTab, replaceUntitledTab, confirmLeave, keepUnsaved, keptTabs, session save and restore |
src/lib/stores/tabSession.ts |
isSessionTabPath, sessionWithKept
|
src/lib/stores/tabs.ts |
replaceTabPath, the Untitled label |
src-tauri/src/unsaved.rs, src-tauri/src/commands/unsaved.rs
|
The store and its four commands |
src/lib/menu/* |
file.newFile (Cmd+N), Save All saving Untitled tabs one by one |
src/lib/mcp/handlers.ts, appState.ts
|
untitled: paths accepted by the editor tools, "kind": "untitled", save_file asking without waiting |
A pseudo tab, not a temporary file. A real file in a temp folder would show up in Git, the Files panel and search, and need cleaning up. A pseudo path keeps every file-only feature away for free.
A separate, small editor. FileView is built around a file on disk: loading, reloading on outside changes, blame and change marks. Guarding each of those for "no file" would make it harder to follow than a short UntitledView.
Write while typing. The page cannot delay Cmd+Q, so waiting for the close would lose text. A 500 ms debounce costs one small write per pause.
Files on disk, not state.json. Texts can be large, and state.json is rewritten often and shared between windows. One file per tab means a write touches only that tab.
Closing a tab by hand still asks. That is a clear choice to drop the text; keeping it silently would surprise people.
Restore by folder for files, by workspace for Untitled tabs. A file's edits belong to the file, so they come back in any workspace holding it. Untitled text has no other home than the workspace it was typed in.
-
src-tauri/src/unsaved.rs: write, read, replace and remove; exact texts; listing order and skipping foreign or broken files; bad paths, big and non-UTF-8 texts. -
src/lib/stores/untitledTabs.test.ts: paths, titles and suggested names. -
src/lib/stores/tabSession.test.ts: Untitled tabs in a saved session,sessionWithKept. -
src/lib/stores/tabs.test.ts:replaceTabPathand the Untitled label. -
src/lib/menu/menuSpec.test.ts,menuState.test.ts: New File on Cmd+N, and no Compare or Local History for an Untitled tab. -
src/lib/stores/settingsData.test.ts: therememberUnsaveddefault.
None yet.
- Update this page and the user page when the save flow, the restore rules or the store format change.
- Retake
new-file-untitled.pngwhen the tab strip changes, andsettings-remember-unsaved.pngwhen the Saving rows change. -
scripts/screenshots.tsanswers theunsaved_*commands in the page, so screenshots never write to your own~/.nibtree/unsaved; keep that in step with new commands.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode