Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Safe saving: the new text is written beside the file and renamed over it, keeping its mode, group, line endings, BOM and final newline; a symlink stays a symlink. If the file changed, was replaced or was deleted since you opened it, nothing is written until you choose Keep editing, Overwrite or Reload. A new file never replaces one that appeared meanwhile.
- 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.
- **`/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
Expand Down
14 changes: 12 additions & 2 deletions docs/design/native-module-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ module behaves identically to its upstream namesake.
| Git | `git_branch`, `git_status`, `git_commit`, `git_state`, `git_metrics` / `git` | Native: `gitBranch`, `gitStatus`; pack `nmsh.vcs` (`stash`, `upstream`) | Covered; commit/state/metrics are candidates only if requested |
| Other VCS | `hg_branch`, `fossil_*`, `pijul_channel`, `vcsh` / `mercurial`, `fossil`, `jujutsu`, `sapling`, `svn`, `plastic`, `dvc` | Pack `nmsh.vcs`: `hg` (bookmark/branch), `jj` (repository state) | **Done: Jujutsu and Mercurial** ([#374](https://github.com/raiseCatError/notMyShell/pull/374)), from declarative `.jj` / `.hg` metadata; the rest defer to a provider |
| Core runtimes | `nodejs`, `python`, `golang`, `rust`, `java`, `package` / `node`, `python`, `golang`, `rust`, `java`, `project` | Native: pack `nmsh.project` (`package`, `node`, `python`, `go`, `rust`, `java`) with requested vs. active versions | Covered |
| More language runtimes | `ruby`, `php`, `dotnet`, `swift`, `kotlin`, `zig`, `elixir`, `lua`, `haskell`, `dart`, `deno`, `bun`, `scala`, `perl`, `ocaml`, `nim`, `julia`, `r`, `crystal` | Only generic: built-in `toolchain` and `discoveredTools` show that a project uses a tool, not its version | **Done: Ruby, PHP, .NET, Swift, Zig, Deno ([#370](https://github.com/raiseCatError/notMyShell/pull/370)); Elixir, Dart, Flutter ([#372](https://github.com/raiseCatError/notMyShell/pull/372)). Remaining candidates: Bun, Kotlin/Scala, Lua, Haskell.** Same pattern as `runtimes.ts`: requested version from the project's own file, active version from the install path or the runtime's metadata, never by running it |
| More language runtimes | `ruby`, `php`, `dotnet`, `swift`, `kotlin`, `zig`, `elixir`, `lua`, `haskell`, `dart`, `deno`, `bun`, `scala`, `perl`, `ocaml`, `nim`, `julia`, `r`, `crystal` | Only generic: built-in `toolchain` and `discoveredTools` show that a project uses a tool, not its version | **Done: Ruby, PHP, .NET, Swift, Zig, Deno ([#370](https://github.com/raiseCatError/notMyShell/pull/370)); Elixir, Dart, Flutter ([#372](https://github.com/raiseCatError/notMyShell/pull/372)); Bun (v0.21: `.bun-version`, `package.json` `packageManager`/`engines.bun`, the install location). Remaining candidates: Kotlin/Scala, Lua, Haskell.** Same pattern as `runtimes.ts`: requested version from the project's own file, active version from the install path or the runtime's metadata, never by running it |
| Build systems | `cmake`, `meson`, `gradle`, `maven`, `buf`, `xmake`, `bazel` / `cmake`, `gradle`, `mvn`, `bazel`, `nx` | Gradle/Maven appear as the `java` module's build tool; others only as discovered tools | Covered for Java; **candidate: a single `build` module**: done ([#373](https://github.com/raiseCatError/notMyShell/pull/373): Bazel, Gradle, Maven, CMake, Meson, Make) rather than one module each |
| Environment managers | `conda`, `direnv`, `mise`, `nix_shell`, `pixi`, `guix_shell`, `spack`, `singularity`, `container` / `nix-shell` | Native: `direnv` and `tools` (pack `nmsh.environment`); Python environments including conda/venv inside `python` | Covered for direnv/conda/venv; **candidates: mise, Nix shell, devcontainer/container** |
| Environment managers | `conda`, `direnv`, `mise`, `nix_shell`, `pixi`, `guix_shell`, `spack`, `singularity`, `container` / `nix-shell` | Native: `direnv` and `tools` (pack `nmsh.environment`); Python environments including conda/venv inside `python` | Covered for direnv/conda/venv; mise through `tools`. **Done in v0.21: Nix shell** (`IN_NIX_SHELL`, pure/impure) **and container** (Docker/Podman marker files, Codespaces and dev container host variables; pack `nmsh.system`) |
| Cloud and infrastructure | `aws`, `gcloud`, `azure`, `kubernetes`, `terraform`, `helm`, `pulumi`, `docker_context`, `openstack`, `nats`, `opa` / `aws`, `az`, `gcp`, `kubectl`, `terraform`, `helm`, `pulumi`, `docker`, `argocd`, `cf`, `firebase` | Native: built-ins `kubeContext`, `dockerContext`; packs `nmsh.cloud` (`aws`, `gcp`, `azure`), `nmsh.infrastructure` (`terraform`, `helm`, `pulumi`) | Covered for the common set; ArgoCD, Firebase, Cloud Foundry, OpenStack, NATS, OPA: provider |
| Frameworks and app tooling | `angular`, `react`, `svelte`, `tauri`, `flutter`, `unity`, `umbraco` (Oh My Posh) | None | **Do not duplicate.** These are project-type labels derived from the same files the runtimes read; they add chrome, not information |
| Package managers | `npm`, `pnpm`, `yarn`, `bun` (OMP) | `package` module reports the package and its manager | Covered |
Expand All @@ -55,3 +55,13 @@ directories (including hostile and symlinked files), and the explain/timings vie
Starship, Oh My Posh and Powerlevel10k keep working as external prompt providers; nothing here replaces them. Importing
their *visual* configuration into NMSh themes is a separate problem owned by
[#304](https://github.com/raiseCatError/notMyShell/issues/304); translating their segment *logic* is not attempted.

## v0.21 review (2026-10-18)

Bun, Nix shell and container were added with the same rules (declarative files, allowlisted variables and marker-file
presence only; nothing run). The `requires` bound of a Context Pack rose from 16 to 24 so a pack can name as many
capabilities as it may hold modules. Kotlin/Scala, Lua and Haskell stay candidates: each needs a declarative version
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.
4 changes: 2 additions & 2 deletions docs/development/v021-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Automated results here are not physical-terminal validation.
| 3 | `/edit` | #402 | `feature/v021-edit` (stacked on slice 2) | | PR open |
| 4 | Semantic Output | #386 | `feature/v021-semantic` (stacked on slice 3) | | PR open |
| 5 | `/track` | #393 (#394) | `feature/v021-track` (stacked on slice 4) | | PR open |
| 6 | Modules and diagnostics | #364, #365 | | | |
| 6 | Modules and diagnostics | #364, #365 | `feature/v021-modules` (stacked on slice 5) | | PR open |
| 7 | Themes and controls | #401, #336 | | | |

## Decisions
Expand All @@ -40,7 +40,7 @@ Automated results here are not physical-terminal validation.
Checklist: [v0.21 physical QA](../testing/v021-physical-qa.md). Slice 1: section 1 (multi-row progress in Ghostty
and Terminal.app, queued highlight colors). Slice 2: section 2 (`/view` keys, rendering, refusals, hostile text,
NO_COLOR). Slice 3: section 3 (`/edit` editing keys, saving, conflicts, refusals). Slice 4: section 4 (overview rows, details
toggle, copy flags, Off). Slice 5: section 5 (`/track` block, refresh, copy, stop, cleanup).
toggle, copy flags, Off). Slice 5: section 5 (`/track` block, refresh, copy, stop, cleanup). Slice 6: section 6 (Bun, Nix, container, reports).

## Verification notes

Expand Down
11 changes: 11 additions & 0 deletions docs/testing/v021-physical-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
| 4 Semantic Output | Ghostty | macOS | zsh | | Not run | |
| 5 `/track` | Ghostty | macOS | zsh | | Not run | |
| 5 `/track` | Terminal.app | macOS | Fish | | Not run | |
| 6 Modules | Ghostty | macOS | zsh | | Not run | |

## 1. Multi-row progress redraws (#388)

Expand Down Expand Up @@ -103,3 +104,13 @@
- [ ] 5.7 An unknown command asks for Yes first; `rm`, `sudo`, `cd`, `$(...)`, `&` and heredocs are refused.
- [ ] 5.8 Close the window with a tracker running: no `sh` or command processes are left (`ps`). `/watch git status`
still opens its manager.

## 6. Native modules and diagnostics

- [ ] 6.1 In a folder with `bun.lock` and `"packageManager": "bun@1.1.30"`, `/modules` recommends Bun; enabled, typing
`bun` shows its version (requested, and active when installed through Homebrew).
- [ ] 6.2 `nix-shell -p hello` (or `nix develop`) with the Nix shell module enabled: it shows `impure`; it disappears
after `exit`.
- [ ] 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).
3 changes: 3 additions & 0 deletions src/app/TerminalApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ import {runDoctor} from '../doctor/doctor.js';
import {parseWatch, watchSafety, WatchTasks, type WatchSafety} from '../tasks/WatchTasks.js';
import {renderWatchPanel, watchPanelKey, watchRow, type WatchPanelState} from '../tasks/WatchPanel.js';
import {finalResultRows, trackBlockRows} from '../tasks/trackBlock.js';
import {explainModules, moduleTimings} from '../prompt/moduleReport.js';
import {analyzePaste, KIND_LABELS, needsPreview, PASTE_EXACT_NOTE, pasteHeader, primaryKind, type PasteAnalysis, nmshCommandNote} from '../input/pasteGuard.js';
import type {BatchShell} from '../input/pasteBatch.js';
import {createPasteReview, displaySafe, pasteReviewKey, renderPasteReview, type PasteReviewState} from '../input/PasteReview.js';
Expand Down Expand Up @@ -2944,6 +2945,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 === '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(); }
else if (slash.kind === 'screensaver') {
Expand Down
7 changes: 4 additions & 3 deletions src/commands/slashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const RAW_COMMANDS: readonly SlashCommand[] = [
{name: '/appearance', insertion: '/appearance', description: 'Configure terminal appearance'},
{name: '/motion', insertion: '/motion', description: 'Motion: context transitions, command launch, completion highlight and effects (same as /appearance → Motion)'},
{name: '/prompt', insertion: '/prompt', description: 'Configure prompt provider and composer layout'},
{name: '/modules', insertion: '/modules', description: 'Context modules, catalog and Context Packs: show, route to a surface, search; saved as you change them'},
{name: '/modules', insertion: '/modules', description: 'Context modules, catalog and Context Packs: show, route to a surface, search; saved as you change them (/modules explain · /modules timings)'},
{name: '/cursor', insertion: '/cursor', description: 'Text caret shape and blink while NMSh owns the composer'},
{name: '/activity', insertion: '/activity', description: 'Live activity colors for the running-command line'},
{name: '/screensaver', insertion: '/screensaver', description: 'Idle visuals: live gallery, timeout and colors'},
Expand Down Expand Up @@ -178,7 +178,7 @@ export type ParsedSlashCommand =
| {kind: 'appearance'}
| {kind: 'motion'}
| {kind: 'prompt'}
| {kind: 'modules'}
| {kind: 'modules'; report?: 'explain' | 'timings'}
| {kind: 'chroma'}
| {kind: 'theme'}
| {kind: 'themeBridge'}
Expand Down Expand Up @@ -281,7 +281,8 @@ export function parseSlashCommand(input: string): ParsedSlashCommand | undefined
if (/^\/appearance\s*$/u.test(input)) return {kind: 'appearance'};
if (/^\/motion\s*$/u.test(input)) return {kind: 'motion'};
if (/^\/prompt\s*$/u.test(input)) return {kind: 'prompt'};
if (/^\/modules\s*$/u.test(input)) return {kind: 'modules'};
const modules = /^\/modules(?:\s+(explain|timings))?\s*$/u.exec(input);
if (modules) return modules[1] ? {kind: 'modules', report: modules[1] as 'explain' | 'timings'} : {kind: 'modules'};
if (/^\/chroma\s*$/u.test(input)) return {kind: 'chroma'};
if (/^\/theme\s*$/u.test(input)) return {kind: 'theme'};
if (/^\/theme-bridge\s*$/u.test(input)) return {kind: 'themeBridge'};
Expand Down
21 changes: 20 additions & 1 deletion src/context/capabilities/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,23 @@ export const direnv = defineCapability<DirenvFact>({
},
});

export const ENVIRONMENT_CAPABILITIES = [toolVersions, direnv] as CapabilityDefinition<unknown>[];
export interface NixShellFact {
/** `pure` / `impure` as nix-shell and nix develop report it; `active` for an older nix that sets only 1. */
state: 'pure' | 'impure' | 'active';
}

export const nixShell = defineCapability<NixShellFact>({
id: 'env.nixShell',
title: 'Nix shell',
reads: ['IN_NIX_SHELL, set in the shell by nix-shell and nix develop (never runs nix)'],
scope: 'session', family: 'metadata', cost: 'cheap', trust: 'session', sensitivity: 'public', persistence: 'snapshot-safe',
fields: ['state'], env: ['IN_NIX_SHELL'], ttlMs: 30_000, timeoutMs: 500, invalidateOn: ['command'],
preview: {state: 'impure'},
async resolve(context) {
const value = context.env.values.IN_NIX_SHELL;
if (!value) return undefined;
return {value: {state: value === 'pure' || value === 'impure' ? value : 'active'}, evidence: 'IN_NIX_SHELL'};
},
});

export const ENVIRONMENT_CAPABILITIES = [toolVersions, direnv, nixShell] as CapabilityDefinition<unknown>[];
16 changes: 15 additions & 1 deletion src/context/capabilities/runtimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,20 @@ export const zigRuntime = simpleRuntime('runtime.zig', 'Zig toolchain', 'zig', [
export const denoRuntime = simpleRuntime('runtime.deno', 'Deno runtime', 'deno', ['.dvmrc', 'deno.json / deno.jsonc presence', '.tool-versions', 'mise.toml'],
{requested: '2.1.0', requestedFrom: '.dvmrc', active: '2.1.4', manager: 'Homebrew'}, async context => versionFile(context, ['.dvmrc']), ['deno.json', 'deno.jsonc', 'deno.lock']);

export const bunRuntime = simpleRuntime('runtime.bun', 'Bun runtime', 'bun', ['.bun-version', 'package.json (packageManager "bun@…", engines.bun)', '.tool-versions', 'mise.toml'],
{requested: '1.1.30', requestedFrom: 'package.json', active: '1.1.34', manager: 'Homebrew'}, async context => {
const file = await versionFile(context, ['.bun-version']);
if (file) return file;
const manifest = await findNearest(context, ['package.json']);
const pkg = manifest ? parseJsonData(await readMetadataText(manifest.path, LARGE_METADATA_BYTES)) : undefined;
if (!record(pkg)) return undefined;
// Corepack's pin names the package manager and its exact version; engines.bun is a range.
const pinned = /^bun@(\d[\w.+-]{0,31})$/u.exec(text(pkg.packageManager, 64) ?? '')?.[1];
if (pinned) return {version: pinned, from: 'package.json packageManager'};
const range = record(pkg.engines) ? text(pkg.engines.bun, 64) : undefined;
return range ? {version: range, from: 'package.json engines'} : undefined;
}, ['bun.lockb', 'bun.lock', 'bunfig.toml']);

/** A release version as an SDK's own version file states it (Dart's `version`, Flutter's `version`): one bounded line, nothing else. */
const RELEASE = /^v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]{1,32})?)$/u;
async function releaseFile(path: string): Promise<string | undefined> {
Expand Down Expand Up @@ -436,5 +450,5 @@ export const flutterRuntime = runtimeCapability('runtime.flutter', 'Flutter SDK'
return value ? {value, evidence: [requested?.from, active.active ? 'flutter on PATH' : undefined].filter(Boolean).join(', ') || 'flutter'} : undefined;
});

export const RUNTIME_CAPABILITIES: readonly CapabilityDefinition<unknown>[] = [nodeRuntime, pythonRuntime, goRuntime, rustRuntime, javaRuntime, rubyRuntime, phpRuntime, dotnetRuntime, swiftRuntime, zigRuntime, denoRuntime,
export const RUNTIME_CAPABILITIES: readonly CapabilityDefinition<unknown>[] = [nodeRuntime, pythonRuntime, goRuntime, rustRuntime, javaRuntime, rubyRuntime, phpRuntime, dotnetRuntime, swiftRuntime, zigRuntime, denoRuntime, bunRuntime,
elixirRuntime, dartRuntime, flutterRuntime] as CapabilityDefinition<unknown>[];
Loading
Loading