Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
878e716
Input awareness: say when a command is waiting for you, and route typ…
raiseCatError Oct 9, 2026
ce1f57d
Live fish test: wait for each command's own completion; no pause
raiseCatError Oct 10, 2026
6fd2964
Input awareness: a prompt whose evidence lapses is one prompt, its ti…
raiseCatError Oct 10, 2026
68ea9d7
Live test: a timed read is 'probably waiting' on Linux too
raiseCatError Oct 10, 2026
c4c891e
Input awareness: a lapsed prompt resumes as the same wait, with its o…
raiseCatError Oct 10, 2026
684a3fb
Hand Fish builtin readers authenticated terminal ownership
raiseCatError Oct 11, 2026
991f299
Protect hidden input throughout frontend startup and reattach
raiseCatError Oct 11, 2026
2d96b25
Wait for authenticated startup and clean up failed input fixtures
raiseCatError Oct 11, 2026
9465eb5
Respect explicit temporary storage in verification and diagnostics
raiseCatError Oct 11, 2026
86fb4e4
test: isolate provider availability and mise metadata changes
raiseCatError Oct 11, 2026
566c321
test: isolate home caches and prepare native completion fixtures
raiseCatError Oct 11, 2026
48a0ded
Test session handoff after authenticated shell readiness
raiseCatError Oct 11, 2026
bf8414d
Honor NO_COLOR for restored frontend transcript rows
raiseCatError Oct 12, 2026
c9e7126
fix(history): snapshot authenticated cwd before context refresh
raiseCatError Oct 12, 2026
252018c
test(prompt): isolate provider geometry from checkout paths
raiseCatError Oct 12, 2026
d26b83a
fix(session): validate runtime ownership for every socket request
raiseCatError Oct 12, 2026
820f7ea
Wait for a valid published capture PID before testing cleanup
raiseCatError Oct 12, 2026
1380d25
fix: restore terminal ownership before journal IO
raiseCatError Oct 12, 2026
0b6a6ff
fix: retain durable journal barrier for ordinary startup
raiseCatError Oct 12, 2026
34d3f84
test: resolve isolated tmux fixture from host PATH
raiseCatError Oct 12, 2026
4eb8e24
fix: drain paused session sockets during shutdown
raiseCatError Oct 12, 2026
5206db0
fix: refresh live session facts without losing selection
raiseCatError Oct 12, 2026
9cfdf9a
fix: ignore shell prompts after frontend shutdown
raiseCatError Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- **Input awareness:** a command that waits for an answer is no longer just "Running". NMSh reads the terminal's own state, without touching the program. A password or other no-echo read, or a single-key question such as `e2fsck`'s `Fix<y>?`, is confirmed; on Linux, any blocking read of the terminal is confirmed too. An ordinary line question left open by an idle process is shown as "Probably waiting for input". The question stays visible beside the composer however far the transcript has scrolled, and the window title, `/sessions`, `/resume` and a cross-session notice show the wait. Hidden and single-key input goes straight to the program, keystroke by keystroke, and is never shown or kept. A reply left unsent when the command ends is discarded, never run as a shell command. Waiting time is measured separately from wall-clock time and reported on completion. See `docs/design/input-awareness.md`.

### Fixed
- **Single-key prompts:** answering a single-key question through the composer no longer sends a stray Enter that answered the next question with its default.
- **Passwords:** text typed at a password prompt is no longer drawn in the composer.

## [0.18.0] - 2026-10-09

Context Engine platform, Status Strip 2.0, managed Claude Code, worktrees and a read-only GitHub workspace: demand-driven context modules and Context Packs, `/strip` and `/modules`, `/claude` with structured permissions and questions, `/mods`, `/worktrees`, `/github`, terminal-title ownership, and tmux, passthrough and cursor fixes.
Expand Down
8 changes: 5 additions & 3 deletions docs/architecture/mise-project-awareness.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ command and 128 KiB combined stdout/stderr limit. stderr is discarded; failures
are generic. JSON parsing retains tool names/versions and task names only.
Unknown fields, including `env`, `run`, descriptions and source paths, never
enter UI/cache/persistence. Metadata remains in memory, bounded to 32 identities
keyed by canonical cwd, binary and ancestor marker/project stat identity. Config
changes invalidate an identity; explicit refresh reruns inspection. No typing or
keyed by canonical cwd, binary and ancestor marker/project stat identity. Changes to marker filesystem metadata invalidate an identity. A same-size edit
with unchanged timestamps can retain it; NMSh does not read config content to
detect this. Explicit refresh requires renewed consent and reruns inspection. No typing or
rendering invocation exists. A failed inspection is cached factually.

Task Enter inserts a quoted visible `mise run '<task>'` into the composer. A
Expand All @@ -39,7 +40,8 @@ Task listing may fetch remote task files and evaluates resolved task directories
The consent screen therefore does not describe metadata as side-effect-free.

Standard marker coverage is conservative, not a full reimplementation of mise
config selection: `mise.toml`, `.mise.toml`, `.mise/config.toml`,
config selection: `mise.toml`, `.mise.toml`, `mise.local.toml`, `.mise.local.toml`,
`.mise/config.toml`,
`.config/mise/config.toml`, `.tool-versions`. Environment-specific/custom config
may exist without a detected marker. Metadata inspection remains explicitly
available when installed. There is no automatic task argument editor, active
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/shell-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ ZshAdapter FishAdapter BashAdapter
| Member | Purpose |
|---|---|
| `launch(context)` | Write private bootstrap files into a per-session directory and return argv + env. The user's own startup files load first. |
| marker grammar | Every adapter emits the same authenticated OSC 777 markers: `<status>;<cwd>` when a command finished (or the shell became ready) and `exec2;<0\|1>;<command>` before a command runs. `ShellProtocolDecoder` is shared. |
| marker grammar | Every adapter emits the same authenticated OSC 777 markers: `<status>;<cwd>` when a command finished (or the shell became ready) and `exec2;<0\|1>;<command>` before a command runs. Fish also emits `reader` from `fish_read` before builtin input starts. `ShellProtocolDecoder` is shared. |
| `editorChrome` | `none` (zsh with ZLE off, Bash `--noediting`): everything after readiness is output. `prompt-to-exec` (Fish): bytes between readiness and exec are Fish's own line editor redrawing, and are dropped. |
| `scrubCommandStart`, `answerQueries` | Fish turns bracketed paste off as it hands the terminal to a command; that byte sequence is removed from the start of command output so it can never reach the host in passthrough. Fish 4 waits for a primary-device-attributes reply while idle; NMSh, being its terminal, answers minimally. |
| `capabilities` | What the backend can honestly provide: completion richness and descriptions, live names, history import, the shell's private-history rule, job counting. The frontend degrades on absent capabilities instead of faking them. |
Expand Down
150 changes: 150 additions & 0 deletions docs/design/input-awareness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Input awareness: when a command needs you

A running command that waits for an answer (`e2fsck`'s `Fix<y>?`, a password, `rm -i`, a `read` prompt) used to look
like any other running command: `Running · 30s`. NMSh now tells the two apart, shows the question where it stays in
view, routes typing the way the program reads it, and keeps waiting time separate from execution time.

Verified on macOS 27 (zsh 5.9, Bash 5.3, Fish 4.9) on 2026-10-10. The Linux kernel layer is implemented and unit-tested
against the kernel's documented `/proc` formats, but it has not been run on a Linux host in this change.

## Principles

- **NMSh stays a frontend over the real shell.** It reads the terminal's own state and the process table. It never
pauses, signals or writes to a program to find out, and it never answers for you.
- **One authoritative state.** `InputWatch` runs beside the PTY: in the session service, or in-process when there is
no service. It publishes one `InputState` that every surface renders: the live activity line, the composer, the
window title, `/sessions`, `/resume` and cross-session notices. No surface infers anything on its own.
- **Silence is not evidence.** A quiet process might be sleeping, computing or waiting on the network. A request needs
positive evidence, and its confidence is shown in words.

## What NMSh can know, and how

| Layer | Signal | Platforms | Confidence |
|---|---|---|---|
| Kernel | A foreground thread is blocked in `read()` on this terminal (`/proc/PID/task/TID/syscall` plus the fd's link). If every thread is blocked elsewhere, that vetoes the other layers. | Linux | confirmed |
| Terminal line settings | `stty -a` on the PTY device: echo off with line input on means a hidden line (password). Line input off with a prompt left open on the cursor's line means single keys (`e2fsck`, `read -k`, `read -n1`). | macOS, Linux | confirmed |
| Prompt evidence | A question-shaped line left open (ends in `?`, `:`, `]`, `)`, `>`), written once and never rewritten or cursor-drawn, output quiet for 0.7 s or more, and the foreground group idle across two samples (nothing running on a CPU, no CPU time gained). | macOS, Linux | likely |

Raw mode alone is never a request: spinners and terminal proxies (`docker run -it`) set it without asking anything. A
program that owns the terminal is never reported, because it draws and reads its own interface. That covers the
alternate screen, bracketed paste, mouse or focus reporting, the kitty keyboard protocol, and NMSh's known full-screen
commands.

**Reliable:** password and other no-echo reads (`sudo`, `ssh`, `getpass`, `read -s`); single-key questions (`e2fsck`,
`read -k1`, `read -n1`, menu libraries that read keys); on Linux, any blocking read of the terminal, including prompts
that end with a newline.

**Inferred (shown as "Probably waiting for input"):** ordinary line reads on macOS, such as `read -p`, `rm -i`, Python
`input()` and `apt`'s `[Y/n]`. macOS exposes no wait channel without privileges (`ps -o wchan` is empty for every
process), so the open question plus an idle process is the best available evidence.

**Not detected generically:**
- Line reads on macOS whose prompt ends with a newline, or that print no prompt at all.
- Programs reading through `poll`/`select` on macOS.
- Programs that own the terminal. Fish 4's own `read` is one: it is fish's line editor, with bracketed paste and OSC 133
marks, so NMSh hands it the terminal and fish answers natively. It is not reported as waiting.

In all of these cases NMSh shows "Running", as before.

**False-positive guards:**
- **Builds:** busy CPU or a process in state `R`.
- **`sleep`, network waits, background tasks:** no question left open.
- **Progress bars:** a carriage-return rewrite marks the line unreliable.
- **Status lines:** `Downloading...` is not question-shaped, and cursor-drawn lines are ignored.
- **Log streams:** no open line, and probes are rate-limited.
- **Questions printed as ordinary output:** later output ends the wait at once.
- **Interactive full-screen or inline programs:** they own the terminal.

### Cost

Probes start only after a running command's output goes quiet:
- **First look:** 150 ms after quiet with a line left open, 400 ms otherwise.
- **Then:** at 1 s and 2.5 s, every 3 s, and every 10 s after a minute of quiet.
- **Closed line:** at most one look every 2 s.

Each look runs `/bin/stty -a` and `/bin/ps` (plus `/proc` reads on Linux) with fixed arguments and a 1.5 s timeout. A
probe that started before newer output is discarded.

## States and time

- **Running:** no credible request. The line keeps its shimmer and adds `· waited 18s` once the command has waited.
- **Waiting for input** (◆, accent and weight) or **Probably waiting for input** (◇): the attention line plus the open
question.
- **Running again:** after an answer (a key in key mode, Enter otherwise, or Ctrl+C or Ctrl+D), or when the program
writes again.
- **Completed, failed or interrupted:** unchanged.

`durationMs` stays the whole wall-clock time. Waiting is measured separately:
- **Start:** when the program wrote the prompt it waits on.
- **End:** at the answer, at new output, or at completion.
- **Evidence that simply disappears:** if a read times out silently, the wait ends at the look that saw it gone, so
that one boundary is accurate to within a probe interval.

Intervals are summed per command and carried with the completing prompt (`inputWaitMs`, `inputWaits`), so they survive
detach and replay. They are recorded on the transcript record, and the completion row reads
`✔ Completed · 1m 25s · 18s waiting for input (2 prompts) · 15:42`. Active time is not shown: waiting is the only
boundary NMSh observes.

## Presentation

The activity region already reserves two rows while a command runs (the working line and a spacer). A wait uses both,
so nothing new is reserved and the PTY never resizes:

```
◆ Waiting for input · e2fsck · 18.0s · 1m 40s total
Padding at end of inode bitmap is not set. Fix<y>? · each key goes straight to e2fsck
❯ Keys go straight to e2fsck
```

- **The question stays in view** however far the transcript has scrolled or folded.
- **Narrow widths:** the line drops detail first (total time, then program name); the question outranks the hint.
- **No animation:** no shimmer and no flashing. The timer only counts.
- **NO_COLOR:** keeps the weight and the glyph.
- **Safe glyphs:** `!` for confirmed and `?` for likely.
- **Window title:** reads `◆ e2fsck needs input · project`, so a background tab shows it.

## Input ownership

- **Line input** (confirmed or likely): the composer is the line editor, as before. Enter sends the line to the running
program, never to the shell. A reply that is still unsent when the command ends is discarded with a note. It never
runs as the next command.
- **Hidden lines and single keys** (confirmed): each key goes straight to the program as the bytes a plain terminal
sends, so line-discipline editing, Enter-as-default and no-echo all behave natively.
- The composer shows only where typing goes. A draft typed before the question appeared stays hidden and untouched.
- Keys typed after the question appeared, in the moment before NMSh knew how the program reads, are handed over as a
terminal would have delivered them.
- Ctrl+C, Ctrl+D and Ctrl+Z keep their meaning, and PageUp/PageDown and the wheel still scroll the transcript.
- NMSh gestures and pointer events are never sent.
- Nothing typed is shown, journaled, put in history or written to the key-debug log.
- **There are no answer buttons.** NMSh never submits an answer on your behalf.

## Other sessions

The service is the one owner of every session's state:
- **`/sessions` and `/resume`** lead the row with `◆ Waiting for input` and the question.
- **Cross-session notice:** a wait that lasts 3 s becomes one sticky notice in other windows, for example
`Mango · e2fsck is waiting for input · 2m · /resume`. It is withdrawn when the wait ends or the session is focused.
- **Returning:** go back through `/resume`, which attaches. Attaching shows the waiting question; nothing is answered
automatically.
- **Older frontends:** the frontend lists `input-state` in its hello. An older frontend gets neither the new message
nor the new notice kind, so it never rejects a frame.

## Managed agents

Managed Claude sessions (`/claude`) do not run in a PTY. Their permission requests and questions arrive as structured
events, and the agent view already marks them with the same ◆ attention. Input awareness never parses an agent's
screen. A coding agent run as a shell command (`claude`, `codex`) owns the terminal, so NMSh hands it over and reports
it through its own attention signals (bell, OSC 9/777), not as "waiting for input".

## Files

- `src/session/InputWatch.ts`: the decision and timing (one per session, beside the PTY).
- `src/session/terminalProbe.ts`: read-only `stty`, `ps` and `/proc` facts.
- `src/session/openLine.ts`: the open line and the prompt shape.
- `src/session/inputState.ts`: the shared state.
- Protocol, service and clients: `SessionProtocol.ts` (`input-state`, `CLIENT_FEATURES`), `SessionService.ts`,
`SocketSessionClient.ts`, `InProcessSessionClient.ts`.
- `src/status/inputStatus.ts`: wording and painting.
- `src/terminal/keyBytes.ts`: direct key bytes.
- Tests: `tests/inputAwareness.test.ts` (unit) and `tests/inputAwarenessLive.test.ts` (real PTY: zsh, Bash, Fish, detach
and reattach, other windows, narrow, NO_COLOR and Safe). The harmless e2fsck-style fixture is `tests/fixtures/askyn.py`.
14 changes: 14 additions & 0 deletions docs/development-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,17 @@ Final shard runs passed in 52.2s and 79.9s with four workers. Every shard and re
all assertions and budgets. Higher local fan-out exposed intermittent existing
completion, resume-browser latency and terminal-mode timing failures; focused
reruns and final bounded-worker verification passed without assertion changes.

### Explicit temporary storage

For verification on another disk, set `NMSH_TEST_TMPDIR` to an existing short
absolute directory on a filesystem supporting Unix sockets and permissions.
The runner creates and cleans its private `nt-*` directories there. Test children
receive a private home inside that root so shell completion caches and other
home-relative test artifacts cannot write to the host home. An invalid
directory fails instead of falling back to the system disk. Keep paths short:
nested PTY/session socket names still have the operating system's length limit.

Set `TMPDIR`, `TMP` and `TEMP` to that directory too for launchers and benchmark
helpers. Point npm and tool caches at the same disk when needed. Default CI
storage and all verification gates remain unchanged.
31 changes: 31 additions & 0 deletions docs/development/fish-typeahead.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Fish keystroke loss after `read`

## Fish 3.7 input ownership (2026-10-11)

The published queue head `7fb6b5a` fails both Fish read tests on Ubuntu 24.04 / Fish 3.7.0:
`tests/inputAwarenessLive.test.ts` (read handover) and `tests/typingAfterRead.test.ts`. Reproduced in a disposable
Linux arm64 VM with Node 26.10.0, independently of the Ubuntu amd64 Actions run `38071796100`. The answer remains
in NMSh's composer or queue while Fish's reader waits. This is a routing bug, not the Fish 4 type-ahead limit below.

Fish 3.7's `__fish_config_interactive.fish` explicitly does not enable bracketed paste in `read`; its line editor
therefore does not supply the terminal-mode signal the frontend previously used for handover. Process probes and
an open-line heuristic cannot reliably recognize a reader that redraws its prompt. `answersByDefault` was not the
cause: it correctly keeps ordinary commands in the queue when no input request is known.

The adapter now uses Fish's own `fish_read` event (also emitted for a read inside a function) to send an authenticated
private `reader` marker. The session broadcasts terminal ownership before forwarding the prompt; the frontend
hands over the terminal until Fish returns to its command prompt. This also covers hidden reads and `read; cat`
without enabling extra terminal modes. Detached sessions retain this ownership for reattach. The marker never
enters raw program output, transcript content or clipboard text.

Regressions cover authentication and split markers, ownership lifecycle and serialization, real hidden-reader
detach/reattach, and input after completion. The typing test builds a unique prompt for each iteration: a historical
redraw of an earlier prompt must not authorize sending an answer to the current command.

Separate CI findings: multiline submission must use Fish's `begin`/`end`, rather than the brace group accepted by
Fish 4 and POSIX shells. The Ctrl+C survivor fixture must wait until its trap is installed, use a shell supporting
`read -t` (Ubuntu `/bin/sh` is dash), and remain alive until the assertion releases it. These corrections preserve
the same-shell queue and test actual routing rather than timing a fixed sleep.

Physical Ghostty/Terminal.app interaction remains unvalidated. The following investigation concerns Fish 4 only.

## Historical Fish 4 investigation

Investigated on 2026-10-10 with fish 4.9.3, zsh 5.9 and Bash 5.3 on macOS 27.

## Symptom
Expand Down
3 changes: 2 additions & 1 deletion scripts/platform-benchmarks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {once} from 'node:events';
import {existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync} from 'node:fs';
import {join} from 'node:path';
import {tmpdir} from 'node:os';
import {performance} from 'node:perf_hooks';
import {ShellSession} from '../src/shell/ShellSession.js';
import {connectSession} from '../src/session/connectSession.js';
import {socketPathFor} from '../src/session/runtimeDir.js';

// A short, owned POSIX root avoids Unix-socket limits. Never touch existing roots.
const root = realpathSync(mkdtempSync('/tmp/npb-'));
const root = realpathSync(mkdtempSync(join(process.env.NMSH_TEST_TMPDIR ?? tmpdir(), 'npb-')));
writeFileSync(join(root, '.zshrc'), '');
const env = {...process.env, HOME: root};
const samples: Record<string, number[]> = {'pty-ready': [], 'service-ready': [], 'service-cleanup': []};
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-selection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const suites = {
fast: ['testSharding', 'input', 'keys', 'highlighter', 'shellProtocol', 'sessionProtocol', 'completionModel', 'pathDisplay', 'linuxPlatform'],
// Minimum-runtime gate: actual launcher, persistent shell backends, protocol,
// platform discovery and build/version behavior without long detach scenarios.
node22: ['testSharding', 'buildInfo', 'startupLaunch', 'shellAdapters', 'sessionProtocol', 'shellProtocol', 'linuxPlatform', 'hostProfiles', 'serviceCompat'],
node22: ['testSharding', 'buildInfo', 'startupLaunch', 'shellAdapters', 'sessionProtocol', 'shellProtocol', 'linuxPlatform', 'hostProfiles', 'serviceCompat', 'socketClientLifecycle', 'sessionViewRefresh', 'stoppedSessionPrompt'],
fedora: ['linuxPlatform', 'tools', 'update', 'hostProfiles', 'portabilityUninstall'],
};

Expand Down
Loading
Loading