You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Second slice of #133. #267 stops the overwrite, but a save-time guard is too late to be kind: you switch to a tab, start editing, git pull rewrites the file ten seconds later, and twenty minutes of work later you find out — with both versions now substantial and no good way out. Told at ten seconds, you'd have stopped and picked a side while it cost nothing. When you learn is as much the feature as what you're told.
Depends on #267 for the recorded mtime/length/content state.
Acceptance criteria
Open a file, edit it, then change it on disk from another terminal. Within about a second, and with no keypress from you, the tab shows ⚠ after the name (● EditorTab.cs ⚠) and the status bar says ⚠ EditorTab.cs changed on disk once.
No modal, no focus steal, no beep. You can keep typing through it. The marker is the whole of it.
The file's name is drawn in a warning colour in the editor tab strip and in its Explorer row, so it's visible without reading the glyph.
A Warning scheme is added to all four themes in src/TuiCode.Workbench/Themes/themes.json, alongside the existing Accent and Error, and both call sites read the colour from there rather than hard-coding one.
git checkout between branches that don't touch the file marks nothing — events are verified by reading and compared on content.
Our own save doesn't mark the tab it just wrote.
A writer that truncates-then-rewrites (so the file is briefly empty or half-written) never leaves a ⚠ describing the partial state: events are debounced ~250 ms and then verified by reading.
If a watcher can't be established, the app still works. It's dropped, logged via ILogger, and nothing on screen breaks. Save stops overwriting a file that changed underneath you #267's save guard is untouched either way. (The activation-time fallback is the next slice.)
Open ~20 files across several directories and the session holds single-digit watchers — one non-recursive watcher per distinct open directory, not the repo tree. The last tab in a directory closing tears its watcher down.
Implementation notes
On Linux each FileSystemWatcher is one inotify instance and fs.inotify.max_user_instances (commonly 128) is shared with every other tool the user is running — hence per-directory, non-recursive, torn down on close. On macOS .NET's watcher is FSEvents, so there's no descriptor-per-file cost.
Take the watcher factory from IFileSystem's IFileSystemWatcherFactory, not new FileSystemWatcher. MockFileSystem has no built-in watcher — it throws "MockFileSystem does not have a built-in FileSystemWatcher implementation" and asks you to assign your own factory — so tests supply a fake that raises events on demand, and every rule above is testable without a real disk or a Thread.Sleep.
On Error (InternalBufferOverflowException) as well as on a throw at creation: drop the watcher and log. Don't retry in a loop.
Watcher events arrive on a background thread. Marshal with App.Invoke before touching any view, as SearchView and RevisionPickerView already do (AGENTS.md).
Tab titles are a TG trap: setting a Title doesn't redraw its header — clear the cached width and SetNeedsLayout(), which is exactly what EditorTab.UpdateTitle already does. Route the marker through it rather than assigning Title directly, or neighbouring headers will overlap when the width changes.
The status-bar slot is the one FileSaved already uses.
Terminal.Gui's VisualRole has no Warning, and none of the four bundled token themes set editorWarning.foreground / list.warningForeground — so the scheme genuinely is new. Pick colours that read in both the light and dark themes.
Tests
With a fake watcher factory on MockFileSystem: a change to a dirty tab marks it; a change with identical content marks nothing; our own save marks nothing; two events inside the debounce window produce one check; a half-written file read mid-event doesn't mark; a watcher that throws on creation leaves the app working and the save guard intact; a watcher Error drops that directory; closing the last tab in a directory disposes its watcher; two tabs in one directory share one watcher; a background tab is marked. Plus: every theme in themes.json defines Warning, and the tab strip and Explorer both resolve their colour from it.
Out of scope
Reloading anything, clean or dirty — the next slice.
The activation-time fallback check — the next slice.
⊘ for a deleted file, Compare/Reload on the save modal, the Reload from disk command — later slices.
Watching the repo tree, or anything recursive.
Watching files that aren't open in a tab: the explorer tree's own refresh and Find results going stale are separate problems.
A modal, a toast, or any prompt at the moment of noticing. Deliberately not.
Context
Second slice of #133. #267 stops the overwrite, but a save-time guard is too late to be kind: you switch to a tab, start editing,
git pullrewrites the file ten seconds later, and twenty minutes of work later you find out — with both versions now substantial and no good way out. Told at ten seconds, you'd have stopped and picked a side while it cost nothing. When you learn is as much the feature as what you're told.Depends on #267 for the recorded mtime/length/content state.
Acceptance criteria
⚠after the name (● EditorTab.cs ⚠) and the status bar says⚠ EditorTab.cs changed on diskonce.Warningscheme is added to all four themes insrc/TuiCode.Workbench/Themes/themes.json, alongside the existingAccentandError, and both call sites read the colour from there rather than hard-coding one.⚠already there.⚠tab raises Save stops overwriting a file that changed underneath you #267's modal, as it already does.git checkoutbetween branches that don't touch the file marks nothing — events are verified by reading and compared on content.⚠describing the partial state: events are debounced ~250 ms and then verified by reading.ILogger, and nothing on screen breaks. Save stops overwriting a file that changed underneath you #267's save guard is untouched either way. (The activation-time fallback is the next slice.)Implementation notes
FileSystemWatcheris one inotify instance andfs.inotify.max_user_instances(commonly 128) is shared with every other tool the user is running — hence per-directory, non-recursive, torn down on close. On macOS .NET's watcher is FSEvents, so there's no descriptor-per-file cost.IFileSystem'sIFileSystemWatcherFactory, notnew FileSystemWatcher.MockFileSystemhas no built-in watcher — it throws "MockFileSystem does not have a built-in FileSystemWatcher implementation" and asks you to assign your own factory — so tests supply a fake that raises events on demand, and every rule above is testable without a real disk or aThread.Sleep.Error(InternalBufferOverflowException) as well as on a throw at creation: drop the watcher and log. Don't retry in a loop.App.Invokebefore touching any view, asSearchViewandRevisionPickerViewalready do (AGENTS.md).Titledoesn't redraw its header — clear the cached width andSetNeedsLayout(), which is exactly whatEditorTab.UpdateTitlealready does. Route the marker through it rather than assigningTitledirectly, or neighbouring headers will overlap when the width changes.FileSavedalready uses.VisualRolehas noWarning, and none of the four bundled token themes seteditorWarning.foreground/list.warningForeground— so the scheme genuinely is new. Pick colours that read in both the light and dark themes.Tests
With a fake watcher factory on
MockFileSystem: a change to a dirty tab marks it; a change with identical content marks nothing; our own save marks nothing; two events inside the debounce window produce one check; a half-written file read mid-event doesn't mark; a watcher that throws on creation leaves the app working and the save guard intact; a watcherErrordrops that directory; closing the last tab in a directory disposes its watcher; two tabs in one directory share one watcher; a background tab is marked. Plus: every theme inthemes.jsondefinesWarning, and the tab strip and Explorer both resolve their colour from it.Out of scope
⊘for a deleted file,Compare/Reloadon the save modal, the Reload from disk command — later slices.