Skip to content

feat: terminal trace setting + shutdown/restore stability fixes#36

Merged
AThraen merged 4 commits into
mainfrom
feat/terminal-diagnostics
May 13, 2026
Merged

feat: terminal trace setting + shutdown/restore stability fixes#36
AThraen merged 4 commits into
mainfrom
feat/terminal-diagnostics

Conversation

@AThraen
Copy link
Copy Markdown
Contributor

@AThraen AThraen commented May 13, 2026

Summary

  • DebugTerminalTrace setting under Settings > Diagnostics (off by default). When on, TerminalBridge logs per-keystroke / per-output-chunk timing plus WPF dispatcher latency to crash.log under [DEBUG-tt] so intermittent terminal-input freezes can be diagnosed after the fact. Zero cost when off.
  • OnClosing SQLite NRE fix. OutputIndexer.Dispose now drains its writer task (2s bounded wait) before MainWindow closes the shared SqliteConnection, so pending FTS5 INSERTs finish first. _db.Close/Dispose also wrapped in try/catch — defensive, since SqliteConnection.Close has been observed to NRE internally during shutdown regardless of the indexer race.
  • WebView2 restore UX. Bulk restore now batches UnauthorizedAccessException from the WebView2 stack into one consolidated dialog at end of restore instead of N "Restore Error" popups, with a message pointing at the likely cause (another running instance locking the WebView2 user-data folder).

Context

User reported an intermittent ~few-minute freeze when typing into a single terminal while others stayed responsive. Crash log audit didn't surface the freeze event itself (current logging is blind to it) but exposed the two latent shutdown/restore bugs above. The trace setting is the diagnostic loop for the original report — next occurrence will pin which stage (xterm.js, PTY, dispatcher) is stalling.

Test plan

  • dotnet build src/CodeShellManager/CodeShellManager.csproj — 0 errors
  • dotnet test tests/CodeShellManager.Tests/ — 82/82 pass
  • Manual: toggle Settings > Diagnostics > "Trace terminal input/output" on, type, confirm [DEBUG-tt] lines in %AppData%\CodeShellManager\crash.log
  • Manual: close the app cleanly — confirm no unhandled exception in crash.log

🤖 Generated with Claude Code

Adds opt-in AppSettings.DebugTerminalTrace (off by default) under
Settings > Diagnostics. When on, TerminalBridge logs per-keystroke
and per-output-chunk timing plus WPF dispatcher latency to crash.log
under the [DEBUG-tt] prefix, so intermittent terminal-input freezes
can be diagnosed after the fact. Zero cost when off.

Also fixes two latent bugs surfaced while auditing crash.log:

- OutputIndexer.Dispose now drains its writer task (2s bounded wait)
  before MainWindow closes the shared SqliteConnection, so pending
  FTS5 INSERTs finish first. _db.Close/Dispose in OnClosing are also
  try/caught defensively — SqliteConnection.Close has been observed
  to NRE internally during shutdown regardless of the indexer race.

- Bulk restore now batches WebView2 UnauthorizedAccessException into
  a single consolidated dialog at end of restore instead of N
  "Restore Error" popups, with a message pointing at the likely
  cause (another running instance locking the WebView2 user-data
  folder).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a diagnostic setting to trace terminal I/O timing, and improves shutdown/restore stability by draining the output indexer on dispose and consolidating WebView2 restore errors.

Changes:

  • Introduce AppSettings.DebugTerminalTrace and surface it in Settings UI.
  • Add [DEBUG-tt] tracing hooks in TerminalBridge for input/output timing and dispatcher latency.
  • Improve shutdown/restore behavior: drain OutputIndexer on dispose, guard SQLite close/dispose, and batch WebView2 access-denied restore popups into one dialog.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/CodeShellManager/Views/SettingsWindow.xaml.cs Wires DebugTerminalTrace to the edited settings model and save flow.
src/CodeShellManager/Views/SettingsWindow.xaml Adds a Diagnostics section with a new checkbox for terminal tracing.
src/CodeShellManager/Terminal/TerminalBridge.cs Implements trace logging for terminal input/output timing and dispatcher latency.
src/CodeShellManager/Terminal/OutputIndexer.cs Drains the writer task briefly during dispose to reduce shutdown races with SQLite.
src/CodeShellManager/Models/AppState.cs Adds the DebugTerminalTrace setting to persisted app settings.
src/CodeShellManager/MainWindow.xaml.cs Batches WebView2 restore access-denied errors and adds defensive SQLite close/dispose handling on shutdown.
Comments suppressed due to low confidence (1)

src/CodeShellManager/Terminal/TerminalBridge.cs:206

  • The dispatcher callback does synchronous file I/O via Trace(...) before posting to WebView2. Since this runs on the UI thread, enabling tracing can distort the very dispatcher-latency being measured and may exacerbate stalls. Prefer capturing timestamps here but writing logs off-thread (or at least after the critical UI work).
        string json = JsonSerializer.Serialize(new { type = "output", data = rawData });
        long enqueueAt = DebugSettings?.DebugTerminalTrace == true ? Environment.TickCount64 : 0;
        int len = rawData.Length;
        WpfApplication.Current?.Dispatcher.BeginInvoke(() =>
        {
            if (enqueueAt != 0)
                Trace($"OUTPUT post dispatcher-latency={Environment.TickCount64 - enqueueAt}ms len={len}");
            try { _webView.CoreWebView2?.PostWebMessageAsString(json); }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +61 to +73
private void Trace(string msg)
{
if (DebugSettings?.DebugTerminalTrace != true) return;
try
{
string path = System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
"CodeShellManager", "crash.log");
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)!);
System.IO.File.AppendAllText(path,
$"[{DateTime.Now:HH:mm:ss.fff}] [DEBUG-tt] {DebugSessionId ?? "?"} {msg}\n");
}
catch { }
Comment on lines +431 to +432
<CheckBox x:Name="DebugTerminalTraceCheck" Content="Trace terminal input/output to crash.log"/>
<TextBlock Text="Logs every keystroke, PTY write, and output chunk with timing to %AppData%\CodeShellManager\crash.log (prefix [DEBUG-tt]). Use to diagnose terminal freezes. Off by default — turn on only when reproducing a problem."
Comment on lines 72 to +81
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_queue.Writer.Complete();
// Drain the worker before returning so any in-flight INSERTs finish before
// the shared SqliteConnection is closed. Bounded wait so a slow/stuck
// worker doesn't hang shutdown — pending writes are non-critical on exit.
try { _worker.Wait(TimeSpan.FromSeconds(2)); }
catch { /* AggregateException from worker exceptions — already swallowed inside */ }
AThraen added 3 commits May 13, 2026 20:30
Conflicts: OnLoaded restore loop and OnClosing teardown both diverged
from the staggered-shutdown work that landed in #37.

Resolutions:
- OnLoaded: keep main's launching-placeholder + sequential staggered
  launch flow; layer this PR's batched WebView2-access-denied dialog
  on top so a single consolidated message replaces the previous N
  per-session popups. Drop the duplicate dormant-loop tail (main now
  adds dormant entries up-front before launches start).
- OnClosing: keep main's claude-aware sequential disposal and
  DisposeAndWaitForExitAsync wait; layer this PR's try/catch around
  _db.Close()/_db.Dispose() in to swallow the SqliteConnection NRE
  observed during shutdown.

Review fixes (from PR self-review):
- OnClosing was async void with multiple awaits, which WPF doesn't
  wait for. Switched to the standard e.Cancel=true + reclose pattern
  gated by _shutdownComplete so async cleanup actually finishes before
  the window tears down.
- _lastOutputTickMs in TerminalBridge.OnPtyData was read-modify-written
  without a memory barrier. Replaced with Interlocked.Exchange — atomic
  read+write in one call, and gives the JIT a barrier even though the
  PTY read loop is currently single-threaded.
Previous commit's cancel-and-reclose pattern only gated on _shutdownComplete,
which is set AFTER async cleanup finishes. If the user clicks the close button
a second time while SaveStateAsync / claude disposal is mid-flight, OnClosing
re-enters, falls through the _shutdownComplete=false branch, and runs the full
cleanup sequence a second time concurrently — double SaveStateAsync (state.json
corruption risk), double Dispose on each SessionViewModel (ObjectDisposedException),
double _db.Close (already swallowed by try/catch, but still pointless work).

Add an _isShuttingDown flag set immediately on first entry, before any await.
Re-entries during the async phase still cancel the close (e.Cancel = true) but
return without re-running cleanup.
Two small follow-ups from Copilot review on #36:

- Settings help text now states explicitly that only timing/byte-length
  metadata is logged, never the actual keystroke or output content. The
  old wording ("Logs every keystroke...") could be read as content
  logging.

- In OnPtyData's dispatcher callback, capture the dispatcher latency
  into a local first, then PostWebMessageAsString, then Trace. Previous
  ordering ran sync File.AppendAllText *before* the WebView2 post,
  which delayed terminal rendering whenever tracing was enabled. The
  measurement itself was already taken inside the callback so accuracy
  is unchanged.
@AThraen AThraen merged commit 7f3f813 into main May 13, 2026
1 check passed
@AThraen AThraen deleted the feat/terminal-diagnostics branch May 13, 2026 18:57
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