Skip to content
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Limits: your own regular UTF-8 text files up to 1 MiB with one hard link, writable, with one kind of line ending. Others are refused with the reason and stay readable in `/view`. Extended attributes and ACLs are not carried over by a save. Unsaved text is lost if the NMSh window itself is closed.
- **Semantic Output** for test runs and builds ([#386](https://github.com/raiseCatError/notMyShell/issues/386), first phase). A completed block whose output NMSh recognizes gets one overview row with the counts the tool printed (`✔ node --test · 1,772 passed · 0 failed · 25 skipped`, `✘ TypeScript · 3 errors in 2 files`) and, when it listed failures or diagnostics, a details row that opens them, taken verbatim from the output. Recognized: Node's test runner (spec and TAP), Jest, Vitest, pytest, `cargo test`, `go test`, TypeScript, Rust and GCC/Clang diagnostics; anything else stays raw only, and short passing output gets nothing. Details start open when the raw output is folded; click the details row or use Actions → Show or hide details. The output itself, its folding, search, selection and plain `/copy` are unchanged. `/copy --semantic` copies the reading (raw for an output without one, as the note says) and `/copy --both` the raw output and the reading under labelled NMSh separators. Settings → Transcript → Semantic Output turns it off.
- **Native modules: Bun, Nix shell and container** ([#364](https://github.com/raiseCatError/notMyShell/issues/364)). Bun's requested version comes from `.bun-version` or `package.json` (`packageManager`, `engines.bun`) and the active one from its install location; the Nix shell module shows `pure`/`impure` from `IN_NIX_SHELL`; the container module names Docker, Podman, a dev container or Codespaces from marker files and host variables. None runs anything; all are off until enabled in `/modules`, which recommends Bun and Nix when their files are present.
- **`/modules explain` and `/modules timings`** ([#365](https://github.com/raiseCatError/notMyShell/issues/365)) write a plain-text report into the transcript: each configured module's surface, whether it is on screen and why, and its sources' state, age and last collection time; or the slowest last collections. They read what the Context Engine already has and collect nothing.
- **`/modules explain` and `/modules timings`** ([#365](https://github.com/raiseCatError/notMyShell/issues/365)) write a plain-text report into the transcript: each configured module's surface, whether it is on screen and why (including when the Context Rail or Status Strip left it out, or shortened it, to fit the width in the last frame), and its sources' state, age and last collection time; or the slowest last collections. They read what the Context Engine already has and collect nothing.
- **Five more bundled theme families** ([#401](https://github.com/raiseCatError/notMyShell/issues/401)): Ayu (Dark, Mirage, Light), Kanagawa (Wave, Dragon, Lotus), Everforest (Dark, Light), Flexoki (Dark, Light) and Night Owl (Night Owl, Light Owl), with colors copied exactly from each project's MIT-licensed source at a recorded revision. They theme NMSh's own prompt fills and chrome only; the terminal's own colors and configuration are never changed. Existing theme ids and saved choices are unchanged, and Lavender stays the default. Theme Studio's Built-in list gains `/` to filter by name, family, or `dark`/`light`.
- **`/track`** keeps one live result of a finite command in the transcript, refreshed in place ([#393](https://github.com/raiseCatError/notMyShell/issues/393)). `/track df -h` or `/track --every 5s -- docker system df` writes the command once, then shows a status row (`live · every 5s · updated 13:49:25 · exit 0`, a failing exit, a timeout, paused, stale, refreshing) and the latest result under it, with a stable height. The shell stays free; nothing accumulates per refresh, and find, selection and `/copy` never include the live rows. `/track copy` copies the latest result, `/track refresh`, `pause`, `resume` and `stop` control it, and stopping leaves the last result in the transcript once. Samples run `sh -c` from the starting directory with your shell's PATH (not its aliases or functions), never overlap, and are bounded (200 kB, a timeout, at most 8 trackers); destructive and unknown commands are refused or need a Yes, as for `/watch`. Trackers stop with the window that started them. `/watch` keeps its manager and its behavior; both share one scheduler, which gains stricter interval parsing, refusals for substitutions, background jobs and heredocs, UTF-8-safe bounded capture, visible timeouts and a stop grace period (from draft #394).

### Fixed

- Multi-line progress displays that redraw their own rows (cursor up and erase line, as concurrent downloads and container pulls do) now update in place instead of leaving a copy of every frame in the transcript ([#388](https://github.com/raiseCatError/notMyShell/issues/388)). A program can redraw only its own recent rows, at most a screen height up, never NMSh's lines or earlier commands; `/copy` copies the final rows once. Absolute cursor positioning keeps the previous behavior.
- Multi-line progress displays that redraw their own rows (cursor up and erase line, as concurrent downloads and container pulls do) now update in place instead of leaving a copy of every frame in the transcript ([#388](https://github.com/raiseCatError/notMyShell/issues/388)). A program can redraw only its own recent rows, at most a screen height up, never NMSh's lines or earlier commands; `/copy` copies the final rows once. Rows are the terminal's rows: a line wider than the window counts as the rows it occupies, so a display that redraws over its own wrapped line no longer erases the log lines above it at narrow widths. A status row an installer reserves below a scroll region is not written into the log. Absolute cursor positioning is not mapped.
- A queued command whose command word was still being classified when the next prompt reported new shell knowledge no longer keeps the unclassified color ([#408](https://github.com/raiseCatError/notMyShell/issues/408)).
- With a slow zsh configuration (one that takes longer than a second and a half to load, as nvm or a large framework can), command colors no longer stop for the rest of the session: the first lookup waited on the configuration, timed out, and ended the helper that tells known commands from unknown ones.
- Paste review and `/track`: a redirect into a file is recognized wherever `>/dev/null` also appears (`echo x > ~/.zshrc 2>/dev/null` was labelled read-only), and `>|` is a redirection, not a pipe.
- `/track` and `/watch` no longer start, without asking, commands that change things through the network or another command: `env <command>`, `NAME=value` prefixes, git's `-c`/`--ext-diff`, `npm publish`/`login`/`x`, `pnpm dlx`, `ssh`, `curl -X POST`/`-d`/`-o file`, plain `wget`; options that write files (`git … --output`, `sort -o`, `jest -u`, `--fix`) are refused. Read-only probes (`ping`, `dig`, `nc -z`, a curl GET) still start at once, and `npm test 2>&1` is no longer refused as a background job ([#393](https://github.com/raiseCatError/notMyShell/issues/393)).
- `/track stop` and a closing window end every process a tracked command left in its process group, including one that ignored TERM; a tracker whose directory disappeared names it instead of reporting `spawn /bin/sh ENOENT`.
Expand Down
5 changes: 4 additions & 1 deletion docs/design/native-module-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@ capabilities as it may hold modules. Kotlin/Scala, Lua and Haskell stay candidat
source that is reliable enough to show (a Gradle Kotlin plugin version, a rockspec, a Stack resolver) and was not judged
worth a module per language in this release. Diagnostics (#365): `/modules explain` and `/modules timings` give a
plain-text report of routing, visibility reasons, source states and the slowest last collections, from the engine's
snapshots only.
snapshots only. The Context Rail and Status Strip record how their width fitting treated each routed module in the last
live frame (left out, or shortened), and explain says so; previews never overwrite that record. The Main Prompt and
Right Context truncate their text rather than drop modules, so they have nothing to report; a per-segment truncation
trace there remains future work.
12 changes: 10 additions & 2 deletions docs/design/v021-terminal-intelligence.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,16 @@ lines. Fix: a bounded **live region**: the cursor may move up and down only with
never above the most recent NMSh-authored line or the command's start, and never more than the PTY's screen height
(a terminal cannot address scrollback either). Supported: `CSI A/B/E/F` (cursor up/down/next/previous line),
`ESC M`, `ESC D`, `ESC E`, save/restore (`ESC 7/8`, `CSI s/u`), `CSI J` 0/1 within the region, `CSI K` 0/1/2.
Absolute row addressing (`CSI H/f/d` with a row) is not mapped: inline programs cannot know where their rows are, so
it keeps today's behavior (appends), documented. Copy and search see what the terminal would show at the end; the
**Rows are the terminal's rows** (stabilization): once the frontend reports the PTY's width, a stored line wider than
it occupies several rows, so a program that counts the rows it drew (as progress libraries measure wrapping) moves
over exactly those rows. Moving by stored lines instead climbed past a wrapped line into the program's own earlier
output and erased it at narrow widths (reproduced, and in a real PTY at 40 columns). Carriage return, backspace,
`CSI G/C/D` and `CSI K` act on the cursor's row within its line; a resize re-wraps stored lines at the new width, as
a reflowing terminal does. The contract stays deliberately limited: absolute row addressing (`CSI H/f/d` with a row)
is not mapped, because inline programs cannot know where their rows are (cursor position reports are not answered),
so the cursor stays where it is; scroll regions (`CSI r`) are not emulated, except that text a program draws below a
region it set (a reserved status row, such as an installer's progress line) is not stored, so it cannot leak into the
log rows. Copy and search see what the terminal would show at the end; the
intermediate frames are not kept, as a terminal's scrollback does not keep them. Raw events in the session spool are
unchanged and replay through the same parser. Tests: single and multi-row redraws, interleaved stdout, split
sequences, clamping at the fence, NMSh lines inserted mid-redraw, metadata never edited, a real-PTY run of the
Expand Down
8 changes: 8 additions & 0 deletions docs/testing/v021-physical-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
- [ ] 1.3 Resize the window narrower and wider while 1.2 runs: no earlier command, prompt or NMSh notice is erased.
- [ ] 1.4 `/copy` of that block copies the final rows once, not every frame.
- [ ] 1.5 `printf '10%%\r20%%\r100%%\n'` still shows one `100%` line; `clear` still clears.
- [ ] 1.5a At 40 columns, a progress display whose line is wider than the window (for example `pip install` of a
large wheel, `cargo build` of a crate with a long name, or the wrapped-row fixture in `tests/ansiLiveRegion.test.ts`)
redraws in place and every log line printed before it stays.
- [ ] 1.5b On Linux, `sudo apt install <small package>` (fancy progress on): the transcript keeps the dpkg log lines and
contains no `Progress: [ nn%]` fragments.
- [ ] 1.6 `vim`, `less` and `htop` still take the full screen and return to the composer.
- [ ] 1.7 Queue two quick commands (`ls` then `pwd`) with Ctrl+Q while a short command runs: both stored command
lines end with the known-command color (#408).
Expand Down Expand Up @@ -126,6 +131,9 @@
- [ ] 6.3 In a Docker or Podman container (or a Codespace), the container module names it; on the host it shows nothing.
- [ ] 6.4 `/modules explain` lists every configured module with its surface and reason; `/modules timings` lists the
slowest collections; neither changes what is collected (the Status Strip CPU stays as it was).
- [ ] 6.5 Route three modules to the Context Rail (Modules → Surface), narrow the window until one disappears, then run
`/modules explain`: that module says "left out for width in the last frame"; widen the window and run it again: the
note is gone. The same with the Status Strip.

## 7. Theme families

Expand Down
21 changes: 17 additions & 4 deletions src/app/TerminalApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {queuePresentationChanges, queueRowKey, staleQueueSnapshot} from '../stat
import {shellSubmission} from '../shell/submission.js';
import {railNeedsPromptConversion, railPreviewConfiguration} from '../prompt/railLayout.js';
import {prepareRail, paintRailComposition, railCompositionPreview, type PreparedRail} from '../prompt/railComposition.js';
import {fitOutcome, newFitReport, type FitReport} from '../prompt/fitReport.js';
import {routeModule} from '../context/surfaceRouter.js';
import {promptFacts, moduleFactContext, updateFact, factAllowed} from '../context/facts.js';
import {chromaPreviewNote} from '../appearance/chromaNotes.js';
Expand Down Expand Up @@ -2945,7 +2946,8 @@ export class TerminalApp {
if (this.appearanceHub) { this.appearanceHub.view = 'motion'; this.appearanceHub.selected = 0; this.appearanceHub.previewStart = Date.now(); }
}
else if (slash.kind === 'prompt') { this.panelOrigin = undefined; await this.startPromptSettings(false); }
else if (slash.kind === 'modules' && slash.report === 'explain') this.output.addFrontendBlock(command.trim(), explainModules(this.promptConfiguration.modules, this.modulesContext(), this.promptConfiguration));
else if (slash.kind === 'modules' && slash.report === 'explain') this.output.addFrontendBlock(command.trim(), explainModules(this.promptConfiguration.modules, this.modulesContext(), this.promptConfiguration, Date.now(),
(id, surface) => fitOutcome(surface === 'contextRail' ? this.lastFit.rail : surface === 'statusStrip' ? this.lastFit.strip : undefined, id)));
else if (slash.kind === 'modules' && slash.report === 'timings') this.output.addFrontendBlock(command.trim(), moduleTimings(id => this.contextEngine.status(id)));
else if (slash.kind === 'modules') { this.panelOrigin = undefined; this.openModulesManager(); }
else if (slash.kind === 'chroma') { this.panelOrigin = undefined; this.startChromaSettings(); }
Expand Down Expand Up @@ -4489,7 +4491,10 @@ export class TerminalApp {
if (this.renderRail?.columns === columns) return this.renderRail.prepared;
const configuration = this.passthrough || this.externalPassthrough || this.effectivePromptProvider !== 'nmsh'
? {...this.promptConfiguration, contextRail: {...this.promptConfiguration.contextRail, mode: 'off' as const}} : this.promptConfiguration;
return prepareRail(this.promptContext(), columns, configuration);
const report = newFitReport();
const prepared = prepareRail(this.promptContext(), columns, configuration, 0, report);
this.lastFit.rail = report;
return prepared;
}

private contextRailRows(columns: number): string[] {
Expand Down Expand Up @@ -8467,11 +8472,18 @@ export class TerminalApp {
const record = this.awakeRecord;
const display = configuration.keepAwake.display;
// Modules routed to the strip are resolved facts like every other surface; the strip never collects anything itself.
return renderStatusStrip(configuration.statusStrip, stripStatsFromFacts(this.contextEngine.facts()), columns, undefined,
// The live row records how it fitted routed modules, for /modules explain; previews of a draft do not.
const report = configuration === this.promptConfiguration ? newFitReport() : undefined;
const row = renderStatusStrip(configuration.statusStrip, stripStatsFromFacts(this.contextEngine.facts()), columns, undefined,
record ? {full: awakeLabel(record, display, 'full'), short: awakeLabel(record, display, 'short'), glyph: awakeLabel(record, display, 'glyph')} : undefined,
this.statusStripModuleItems(configuration));
this.statusStripModuleItems(configuration), report);
if (report) this.lastFit.strip = report;
return row;
}

/** How the last live frame fitted routed modules on the Rail and the Strip (#365). */
private readonly lastFit: {rail?: FitReport; strip?: FitReport} = {};

/** Strip-routed modules with their group and, for the path, a shorter form the fitter may use first. */
private statusStripModuleItems(configuration = this.promptConfiguration): StripModuleItem[] {
const context = this.promptContext();
Expand Down Expand Up @@ -10577,6 +10589,7 @@ export class TerminalApp {
this.lastPtyColumns = columns;
this.session.resize(columns, plan.ptyRows);
this.output.setScreenRows(plan.ptyRows);
this.output.setScreenColumns(columns);
}

const wrapped = this.output.wrapped(columns);
Expand Down
Loading
Loading