Fix v0.6 physical-QA failures: multi-session restore, inline interactive UIs, Terminal.app startup - #213
Merged
Conversation
…ive UIs Restoring several detached sessions opened only one and could end NMSh with 'Detected unsettled top-level await' (in Terminal.app, nmsh failed to start). The window launcher awaited osascript after unref()'ing it; with stdin paused after the picker nothing else kept the event loop alive, so Node exited mid-await after spawning only the first launcher. The child now stays referenced while awaited, and a launcher that hangs times out after 15 s so that session's attach command is named instead. Inline interactive terminal UIs, such as Claude Code run through a wrapper like claude-account2, stayed in NMSh's structured view: the classifier handed over the terminal only for the alternate screen or a known name. A running program that turns on terminal input modes (bracketed paste, mouse, focus events, kitty keyboard) now counts as owning the terminal. The mode tracker moves to TerminalModes and records this; the frontend replays the program's modes when it hands over mid-command, and the service treats it like fullscreen for reattach, mode restore and detached output. Progress output and other batch commands are unaffected. Tests reproduce both on the old code, including the built bin/nmsh with a stand-in osascript.
The tmux and screen tests started NMSh inside this checkout, so its prompt ran git status in the repository; killing tmux mid-status left .git/index.lock behind. Run it from the sandbox home with absolute entry and loader paths, as the other live tests do.
6 tasks
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #211
Closes #212
Physical v0.6 QA found three failures. #1 and #3 turned out to be one lifecycle bug.
1 + 3. Multi-session restore opens only one; Terminal.app
nmshexits with "unsettled top-level await" (#212)Root cause:
spawnLauncher(the #197 window launcher) spawnedosascriptand awaited its exit afterchild.unref().awaitwith code 13 and the warning.nmshfailing immediately.Reproduced with the built
bin/nmshand a stand-inosascript: exit 13, warning printed, one launch.Fix (
src/host/terminalHost.ts): the child stays referenced while awaited. A launcher that hangs (for example on a macOS Automation prompt) counts as failed after 15 s, so startup continues and names that session'snmsh --attachcommand. There's no special case for "select all"; the general N-selected path was already correct.2. Inline interactive CLIs stay in the structured view (#211)
Root cause:
CommandClassifierhanded the terminal over only for the alternate screen (?1049h/?47h), andPassthroughPolicyonly for known names. Claude Code draws inline, so underclaude-account2it:…;2m);Evidence-based fix: a running program that turns on terminal input modes owns the terminal: bracketed paste
?2004h, mouse?1000/1002/1003/1005/1006/1015h, focus?1004h, or a kitty keyboard pushCSI > n u. Batch output never does that. Cursor visibility and cursor-key mode are deliberately excluded because progress bars and pagers use them.src/session/TerminalModes.ts(moved out ofSessionService, and still re-exported from it) records these modes, now including the kitty flags, and exposesinteractive/ownsTerminal.src/output/Classifier.ts: input modes, not only the alternate screen, lead to PASSTHROUGH.src/app/TerminalApp.ts: per-command mode tracking; when the terminal is handed over mid-command, the program's earlier modes are replayed to it (the triggering chunk was already written raw).src/session/SessionService.ts: reattach passthrough, the Live sessions: stale-session cleanup, crash recovery and hardening #131 mode restore, and detached-output exclusion all useownsTerminal, so an inline UI also works after detach. The/resumefullscreen label still means the alternate screen.Also: stale
.git/index.lockfrom the multiplexer testsThe #204 tmux and screen tests started NMSh inside this checkout, so its prompt's
git statusran in the repository, and killing tmux mid-status left.git/index.lockbehind (found twice during this work). They now run NMSh from the sandbox home, like the other live tests. After the change, the full suite leaves no lock.Tests
tests/startupLaunch.test.ts:bin/nmshas Terminal.app, with three detached sessions, select all, Enter: this window attaches and the other two launch, with no warning;bin/nmshstarts normally as Terminal.app.tests/interactiveCli.test.ts:git diff --checkare clean. The full suite passed 615/615 four times. No leaked services, tmux or screen.Physical retest (smallest)
nmsh, select all and press Enter. This window attaches one, and each other one opens in its own Ghostty window in the same app./resume.nmshstarts (no "unsettled top-level await"). Repeat step 1 there; allow the Automation prompt once.claude-account2: the trust picker renders cleanly, arrows and Enter work, no NMSh composer shows, and exiting returns to NMSh.claude-account2, close the window, reattach, and it's still interactive.npm testor a longsleeploop still shows NMSh's composer and activity row, not passthrough.