Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9d9033d
feat(launcher): add named envs with --env and envs list|new
jals1212 Oct 13, 2026
9218f6b
feat(shell): show the active named env in the header and Status card
jals1212 Oct 13, 2026
b45cc25
fix(uninstall): protect named envs and prove them in a bundled install
jals1212 Oct 13, 2026
a7a827b
feat(shell): show the named env as its own Status group above Project
jals1212 Oct 13, 2026
639b330
docs(odd): track named environments
jals1212 Oct 13, 2026
aacd381
fix(launcher): label an explicit --home env, use the injected env, an…
jals1212 Oct 13, 2026
e445000
feat(launcher): manage named envs from a full-screen envs TUI
jals1212 Oct 13, 2026
79f44e5
feat(launcher): give the envs gate Gentle's visual identity
jals1212 Oct 13, 2026
a92876b
feat(launcher): list the install's base home in the envs gate
jals1212 Oct 13, 2026
6bb583e
fix(launcher): say a home is set up on first open instead of showing …
jals1212 Oct 13, 2026
7311b66
fix(launcher): name the base home without a marker that needs a legend
jals1212 Oct 13, 2026
81b0090
fix(launcher): mark the default env like /gentle:profiles markers, no…
jals1212 Oct 13, 2026
df75804
fix(launcher): mark the default in envs list as (default), not a lead…
jals1212 Oct 13, 2026
ace01f1
fix(launcher): show text from user files in the envs gate without con…
jals1212 Oct 13, 2026
7fc7fcc
test(launcher): drive the envs manager through a simulated TTY to cov…
jals1212 Oct 14, 2026
d5a32eb
feat(config): resolve Gentle's config home per category for named envs
jals1212 Oct 13, 2026
db6c3a2
feat(launcher): choose per env which Gentle config it owns, like VS C…
jals1212 Oct 13, 2026
56e7114
feat(launcher): VS Code-like config form in the envs gate
jals1212 Oct 13, 2026
5836e74
docs(launcher): document per-env Gentle configuration and envs config
jals1212 Oct 13, 2026
00dca1d
fix(launcher): make the config form say what the copy source needs an…
jals1212 Oct 13, 2026
9fefd40
fix(config): honor env behavior scope in history capture and refuse s…
jals1212 Oct 13, 2026
076daad
fix(launcher): reuse kept own config, validate envs new scope flags, …
jals1212 Oct 14, 2026
26d2234
fix(launcher): clean up a failed envs new, show the env in the regula…
jals1212 Oct 15, 2026
870b98d
Merge branch 'feat/named-envs-core-v2' into feat/named-envs-gate-v2
jals1212 Oct 15, 2026
96cd724
fix(launcher): roll back a failed env rename and keep '..'-prefixed n…
jals1212 Oct 15, 2026
4052028
Merge branch 'feat/named-envs-gate-v2' into feat/named-envs-config-v2
jals1212 Oct 15, 2026
8220a2a
fix(shell): keep the env when the bar narrows and guard a symlinked n…
jals1212 Oct 15, 2026
9cb192e
Merge branch 'feat/named-envs-core-v2' into feat/named-envs-gate-v2
jals1212 Oct 15, 2026
c7b115c
Merge branch 'feat/named-envs-gate-v2' into feat/named-envs-config-v2
jals1212 Oct 15, 2026
fecd68f
fix(launcher): share named envs with the base home's real config, rol…
jals1212 Oct 15, 2026
3d4d746
Merge remote-tracking branch 'origin/main' into feat/named-envs-core-v2
jals1212 Oct 18, 2026
177f75d
Merge branch 'feat/named-envs-core-v2' into feat/named-envs-gate-v2
jals1212 Oct 18, 2026
0002db8
Merge branch 'feat/named-envs-gate-v2' into feat/named-envs-config-v2
jals1212 Oct 18, 2026
4014dce
test(config): keep session scratch ledgers in the shared config home
jals1212 Oct 18, 2026
17d905c
fix(launcher): accept --env only for a real env directory
jals1212 Oct 18, 2026
74402df
fix(uninstall): refuse to remove a folder that holds the named envs s…
jals1212 Oct 18, 2026
bd664d8
Merge branch 'feat/named-envs-core-v2' into feat/named-envs-gate-v2
jals1212 Oct 18, 2026
f72f3b9
fix(shell-bar): keep the env over the brand in the narrowest header
jals1212 Oct 18, 2026
bb193b1
Merge branch 'feat/named-envs-gate-v2' into feat/named-envs-config-v2
jals1212 Oct 18, 2026
b714df0
fix(launcher): drop a deleted env from config.json before removing it
jals1212 Oct 18, 2026
681feb0
docs(launcher): list envs config and scope flags in the envs synopsis
jals1212 Oct 18, 2026
486787b
fix(uninstall): guard every symlink on the named envs path and '..'-n…
jals1212 Oct 18, 2026
778bfe6
Merge branch 'feat/named-envs-core-v2' into feat/named-envs-gate-v2
jals1212 Oct 18, 2026
54bcd4e
test(shell-bar): cover the narrowest header keeping the env beside th…
jals1212 Oct 18, 2026
e2a4dae
Merge branch 'feat/named-envs-gate-v2' into feat/named-envs-config-v2
jals1212 Oct 18, 2026
4addd84
test(launcher): use a junction for the --env link case on Windows
jals1212 Oct 18, 2026
4d4cf6f
Merge branch 'feat/named-envs-core-v2' into feat/named-envs-gate-v2
jals1212 Oct 18, 2026
b64d708
Merge branch 'feat/named-envs-gate-v2' into feat/named-envs-config-v2
jals1212 Oct 18, 2026
42317f7
test(uninstall): run the bundled '..envs' case only where the fixture…
jals1212 Oct 19, 2026
4c5a3af
Merge branch 'feat/named-envs-core-v2' into feat/named-envs-gate-v2
jals1212 Oct 19, 2026
9ff0d9f
Merge branch 'feat/named-envs-gate-v2' into feat/named-envs-config-v2
jals1212 Oct 19, 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
524 changes: 517 additions & 7 deletions bin/gentle-shell.mjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/gentle-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ At 140 columns or wider, fullscreen defaults to a live header over a transcript-
✿ Gentle Shell ⟡ ~/work/gentle-pi main ⟡ gpt-5.5 · medium · team ctx ▰▰▰▰▱▱▱▱ 45% ⟡ $9.49 sub
```

- The header carries live session data: session identity on the left (brand, cwd, branch, dirty count, model · effort · profile) and the two live counters right-aligned (the context gauge and session cost). The effort reads `auto` for NaN models that manage their reasoning depth themselves (DeepSeek V4 Flash, Qwen 3.8 Flash, MiMo), with a one-time notice explaining that the selected level does not change the depth. It never shows the working/thinking state or extension statuses — those stay in the prompt indicator and the Status surface. When the terminal is too narrow for everything, segments give way in a fixed order — profile, then effort, then the whole cwd/branch/dirty group — before the counters are touched; below that, only the brand survives, and below that the header renders nothing.
- The header carries live session data: session identity on the left (brand, the named env when launched with `--env`, cwd, branch, dirty count, model · effort · profile) and the two live counters right-aligned (the context gauge and session cost). The effort reads `auto` for NaN models that manage their reasoning depth themselves (DeepSeek V4 Flash, Qwen 3.8 Flash, MiMo), with a one-time notice explaining that the selected level does not change the depth. It never shows the working/thinking state or extension statuses — those stay in the prompt indicator and the Status surface. When the terminal is too narrow for everything, segments give way in a fixed order — profile, then effort, then the whole cwd/branch/dirty group — before the counters are touched; below that, only the brand and the env survive, and below that the header renders nothing.
- The right rail scrolls **Status → Changes → TODO**. Cards cache their rendered content until their own digest or an explicit invalidation changes. `neon` keeps the rounded border and accent title; `float` uses a tone background, a full-height `▎` accent, one-column transparent margins, painted padding above and below, and a blank painted separator between heading and body. Warning/error/success panels retain their tone colors.
- The Status card carries Project (cwd, branch, session name, active profile), Changes, and Integrations (other extensions' statuses); its live digest detects changes without requiring an event. Model, effort, context, cost, and the per-model usage table live in the header instead — the header ticks every frame, so duplicating them in a card would just make that card repaint every frame too.
- The Status card carries Env (the named env, only when launched with `--env`, shown first), Project (cwd, branch, session name, active profile), Changes, and Integrations (other extensions' statuses); its live digest detects changes without requiring an event. Model, effort, context, cost, and the per-model usage table live in the header instead — the header ticks every frame, so duplicating them in a card would just make that card repaint every frame too.
- Gentle Agents is not part of the rail in any mode: its one card stays above the editor, where it already lived, with fixed right-aligned columns for `model · effort`, tokens, cost, and elapsed, each sized to the widest value among the shown tasks — so the numbers line up vertically even when one row's values are much shorter than another's. A queued task fills only the elapsed column with the word `queued`, leaving the other columns blank rather than overwriting the row.
- The sidebar reuses its last frame until something it paints changes, so silent frames stay cheap; a per-section cache means one card's changing digest (or the header's) never forces an unrelated card to redraw.
- Header placement is configurable. Above input, the float header has a full-width background, painted padding above and below its content, and a transparent `▔` bottom edge. Below input, the float footer groups Changes (when the session's roots have uncommitted work), header data, and locally owned integration statuses under one upper edge; it never duplicates statuses owned by the rail.
Expand Down
48 changes: 47 additions & 1 deletion docs/readme-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ An orphan branch with commits and no parent has no branch point to name as `base
```bash
gentle-shell [options] [-- pi-args...]
gentle-shell home [link|isolated|<path>]
gentle-shell envs [list | new <name> [scope] | config <name> [scope]]
gentle-shell [home selectors] setup [--dry-run]
gentle-shell upgrade [--channel release|main]
gentle-shell upgrade --rollback
Expand All @@ -314,12 +315,13 @@ gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared]
| `--link` | Home is your own pi home: `GENTLE_SHELL_USER_PI_HOME` when a Gentle Shell session recorded it (so `--link` run from inside an isolated session reaches your original pi, not the isolated home), otherwise `PI_CODING_AGENT_DIR` or `~/.pi/agent`. Reuses your existing pi sign-ins, models, and chats; never writes to its `settings.json`. |
| `--isolated` | Home is `GENTLE_SHELL_HOME` or `~/.gentle-shell/agent`, with its own settings in `~/.gentle-shell/gentle-ai` ([config home](#config-home)); a custom `GENTLE_SHELL_HOME` outside a `.gentle-shell` folder keeps `~/.pi/gentle-ai`. No credential seeding. Default when nothing else is configured. |
| `--home <path>` | Home is the given directory. |
| `--env <name>` | Home is the named env `GENTLE_SHELL_ENVS_DIR/<name>` (default `~/.gentle-shell/envs/<name>`). The env must already exist; an unknown name exits 2 instead of creating a new home. See "Named envs" below. |
| `--package-root <dir>` | Force this directory as the gentle-pi package to load, taking over from any conflicting package the target `settings.json` already declares (see "Loading the package" below). |
| `--help`, `-h` | Print usage (flags, commands, env vars) and exit 0. |
| `--version` | Print `gentle-shell <version>`, `pi <version>`, and `home <mode> <dir>`, then exit 0. |
| `--` | Everything after is forwarded to pi verbatim, even text that looks like a `gentle-shell` flag. |

`--link`, `--isolated`, and `--home` are mutually exclusive; combining two is a usage error, as is `--home` or `--home=` with an empty value. Effective-home precedence: an explicit flag wins, then the persisted `home` subcommand choice, then the `--isolated` default. Every argument gentle-shell does not recognize — `--mode rpc`, `-p "..."`, etc. — is forwarded to pi unchanged.
`--link`, `--isolated`, `--home`, and `--env` are mutually exclusive; combining two is a usage error, as is `--home` or `--home=` with an empty value. Effective-home precedence: an explicit flag wins, then the persisted `home` subcommand choice, then the `--isolated` default. Every argument gentle-shell does not recognize — `--mode rpc`, `-p "..."`, etc. — is forwarded to pi unchanged.

### Config home

Expand Down Expand Up @@ -372,6 +374,50 @@ It never overwrites, never follows a symbolic link, never copies `packages`, `ex

Tradeoff: `--link` shares one credential store with pi; `import-pi` duplicates it. A provider that rotates refresh tokens on use may sign out whichever copy refreshes second; run `/login` again in that home.

### Named envs (`envs` subcommand, `--env`)

A named env is a separate agent home managed by gentle-shell under `~/.gentle-shell/envs/<name>/` (`GENTLE_SHELL_ENVS_DIR` overrides the root). Each env has its own sign-ins, models, packages, settings, sessions, and subagent history. The directory is the registry: there is no separate list. Names are 1-40 lowercase letters, digits, `.`, `_`, or `-`, starting with a letter or digit.

| Command | Effect |
| --- | --- |
| `gentle-shell envs` | On a terminal, opens a full-screen env manager. Without a TTY, it behaves like `envs list`. |
| `gentle-shell envs list` | Prints the base home as `[base]`, then every env, each with its setup state; `(default)` after a name marks what a plain `gentle-shell` opens. |
| `gentle-shell envs new <name>` | Creates the env without launching it. Its first launch installs the companion packages, the same as any new `--home`. |
| `gentle-shell --env <name> [...]` | Launches, sets up (`--env <name> setup`), or runs pi subcommands (`--env <name> install npm:x`) in that env. |

The manager lists the install's own home first, as `Gentle Shell base`. It is the `--isolated` home (`GENTLE_SHELL_HOME` or `~/.gentle-shell/agent`), the one Gentle Shell always used before envs, and nothing about it changes. Opening it is `--isolated`, and making it the default persists `"home": "isolated"`, exactly like `gentle-shell home isolated`. Its sessions carry no env label. It cannot be renamed or deleted from the manager, and an env may still be named `base`. The `(default)` marker, like the `(active)` marker in `/gentle:profiles`, names what a plain launch opens: the base home when no `home` is persisted (or it is `isolated` or malformed), the env a persisted path names, and nothing in the list for `link`.

Manager keys: `↑`/`↓` or `j`/`k` select. `enter` launches the selected env. `n` creates an env (after its name, a config form opens; see below), `r` renames it, and `d` deletes it after you type its name. `s` makes it the default. `p` runs `setup`, `i` installs a package source, and `a` runs `pi auth check` for each signed-in provider. `c` opens the config form for the selected env. `q` or `esc` quits. The detail pane shows the path, the setup record, the session count and last session, the signed-in provider ids (never credential values), the config scope, and the declared packages.

Inside a session, the header shows `env <name>` right after the brand, and it stays there as the header narrows. The Status card shows an `Env` group with the name, above `Project`. The launcher passes the name to Pi as `GENTLE_SHELL_ENV`, only for a named env, and clears any inherited value for every other home. The default env is stored as the ordinary persisted `home` path, so a plain `gentle-shell` opens it, and `gentle-shell home link|isolated|<path>` replaces it. Renaming or deleting an env keeps `config.json` consistent: its setup record and default follow it or are removed. Envs are separate homes, not sandboxes. By default they share the repository you work in, Gentle's own configuration (see *Per-env Gentle configuration* below), the Engram memory, and `~/.gentle-ai/state.json`, like every other home. Named envs work the same in a bundled (standalone) install. There, the envs root shares `~/.gentle-shell` with the prefix and launches use the bundled Pi. Both `self-uninstall` variants list the envs root once, as "your named envs", under *Not touched*, and never remove it. They also refuse a plan that would remove anything inside it, for example a `GENTLE_SHELL_HOME` that points into an env.

### Per-env Gentle configuration

Like VS Code profiles, each named env chooses, per category, whether it shares Gentle's own configuration with the base home or keeps its own copy:

| Category | What it covers | Files |
| --- | --- | --- |
| `models` | Per-agent model routing and `/gentle:profiles` | `models.json`, `profiles.json` and their exports |
| `persona` | el Gentleman's persona | `persona.json` |
| `appearance` | `/gentle:customize`: visual settings and profiles, cards, animations, banner, notifications, Vim | see `lib/gentle-config-scope.ts` |
| `behavior` | Permission mode, runtime guardrails, history capture, background subagents, double Esc, builtin codemode opt-out | see `lib/gentle-config-scope.ts` |

Models and profiles stay together because a profile is a snapshot of the routing, and applying one rewrites `models.json`. The update channel and the dev-binary registration belong to the install and are always shared. Repository settings (`<repo>/.pi/gentle-ai/`) stay with the repository.

A shared category follows the base home's configuration live: the bundled install's own config, else `GENTLE_PI_CONFIG_HOME`, else the base home's `~/.gentle-shell/gentle-ai` (the same folder `--isolated` uses since #2201), else `~/.pi/gentle-ai` when the base home has no folder of its own (a custom `GENTLE_SHELL_HOME`). An env launched before the base home ever was still gets that folder seeded once from `~/.pi/gentle-ai`. An own category lives in `<env>/gentle-ai/`. When a category becomes own it is seeded once, by copying from the base home or from another env (that env's effective files, own or shared), or by starting empty with the defaults. A category that is already own is never wiped, and sharing it again keeps its files on disk for later. The choice is stored in `<env>/.gentle-shell-env.json`. An env without that file (every env created before this feature) shares everything.

```bash
gentle-shell envs new client --own persona,appearance # copies them from the base home
gentle-shell envs new lab --own all --empty # owns everything, starting from defaults
gentle-shell envs config client # describes the scope
gentle-shell envs config client --own models --from work # copies work's models & profiles
gentle-shell envs config client --share appearance # shares it again (own files kept)
```

In the manager, `n` opens the same choice as a form after the name: `↑`/`↓` move, `space` or `←`/`→` toggle a category or change the source of new own copies, `a` toggles all, `enter` creates (or applies, for `c` on an existing env), and `esc` cancels. Enter with the defaults shares everything, exactly as before. The base home has no scope: it holds the shared configuration.

When an env owns categories, the launcher passes `GENTLE_SHELL_ENV_CONFIG_HOME` (`<env>/gentle-ai`) and `GENTLE_SHELL_ENV_OWN_CONFIG` to Pi. They win over the bundled config and `GENTLE_PI_CONFIG_HOME` for those categories only. The launcher clears them for every other home, so a nested launch never inherits another env's scope. Scope changes apply from the env's next session. The Engram memory is not part of this yet: all envs still share it.

### Managing packages

`gentle-shell install npm:<pkg>`, `gentle-shell remove ...`, `gentle-shell list`, `gentle-shell update ...`, `gentle-shell config`, and `gentle-shell auth ...` run pi's own commands against the resolved home — the `--isolated` home by default, or your own pi home with `--link`. A launcher flag before the subcommand (`--link`, `--isolated`, `--home <path>`) still selects which home the subcommand runs against. Running `gentle-shell install npm:gentle-pi` inside the isolated home is unnecessary: the launcher already loads the Gentle Shell package itself (see "Loading the package" below).
Expand Down
6 changes: 3 additions & 3 deletions extensions/gentle-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
let ui: ExtensionContext["ui"] | undefined;
let host: { requestRender(): void } | undefined;
let sidebarTui: TUI | undefined;
const visualHome = gentlePiConfigHome(env);
const visualHome = gentlePiConfigHome(env, "appearance");
let agentsVisible = resolveVisualSettings({ gentlePiConfigHome: visualHome }).settings.visibility.agents;
let stopVisualUpdates: (() => void) | undefined;
let sessions: ExtensionContext["sessionManager"] | undefined;
Expand Down Expand Up @@ -1638,7 +1638,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
// bound session used to kill its own non-git writer mid-preparation because
// admission still read only the pin/global layers). Slice 3b-ii: the same
// read covers the frozen inherited profile and `follow` mode.
const profileConfigHome = gentlePiConfigHome(deps.env);
const profileConfigHome = gentlePiConfigHome(deps.env, "models");
const sessionProfile = resolveSessionProfile({
sessionId: ctx.sessionManager.getSessionId(),
cwd: originalCwd,
Expand Down Expand Up @@ -2357,7 +2357,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
freezeSessionProfileAtStartup({
sessionId: ctx.sessionManager.getSessionId(),
cwd: ctx.sessionManager.getCwd(),
configHome: gentlePiConfigHome(deps.env),
configHome: gentlePiConfigHome(deps.env, "models"),
resolveWorktree: deps.resolveWorktree,
env: deps.env,
});
Expand Down
Loading