J301: plain exit reports code 127 and the window does not close #393

Open
opened 2026-11-06 12:19:29 +00:00 by kayg · 9 comments
Owner

Owner report (production, 74b9c4d80, 2026-11-06):

"typing exit in a shell gets me a toast with "Shell exited with code 127" and terminal does not close?"

Windows involved: ssh · shell (root bash on the server's own host) and code.vpn.kayg.… (rootless host code, fish/starship-looking prompt). Not known which one, so both are covered.

Job J301. Scope:

  1. Find which process exits 127 on a plain exit (root bash, and a rootless fish/zsh login), fix the cause, no masking.
  2. What a window does when its shell ends: check D178 (exited sessions keep the final screen and exit code with "Run again"), then implement the owner's expectation for the clean-exit case (status 0 closes the window, no error toast); non-zero keeps the ended state.

Branch fix-shell-exit from origin/dev. Plan: reproduce on compute01 (root clone in /root, rootless as claude), trace, failing tests first, fix, verify quick + affected browser specs with one worker, verify fast. Estimate ~3 h.

Owner report (production, 74b9c4d80, 2026-11-06): > "typing exit in a shell gets me a toast with \"Shell exited with code 127\" and terminal does not close?" Windows involved: `ssh · shell` (root bash on the server's own host) and `code.vpn.kayg.…` (rootless host `code`, fish/starship-looking prompt). Not known which one, so both are covered. Job J301. Scope: 1. Find which process exits 127 on a plain `exit` (root bash, and a rootless fish/zsh login), fix the cause, no masking. 2. What a window does when its shell ends: check D178 (exited sessions keep the final screen and exit code with "Run again"), then implement the owner's expectation for the clean-exit case (status 0 closes the window, no error toast); non-zero keeps the ended state. Branch `fix-shell-exit` from origin/dev. Plan: reproduce on compute01 (root clone in /root, rootless as claude), trace, failing tests first, fix, `verify quick` + affected browser specs with one worker, `verify fast`. Estimate ~3 h.
Author
Owner

Start: branch fix-shell-exit from origin/dev 5f529c3c8.

Finding 1: the agent reports the shell's real status; a plain exit gives 0

Reproduced on the real binary (production build 74b9c4d80 copied from code, and a current dev build) through the real infraternal agent (terminal.open, then typing into the PTY, reading terminal.exited):

path shell input status
compute01 root, no person (direct root shim) bash exit 0
compute01 root, person → root (system login worker, unit infraternal-login-…-0) bash exit 0
compute01 rootless claude, person bash exit 0
code rootless kayg, person, kayg's real zsh config, user-manager env, inside a user scope zsh + starship exit as the first command 0
code rootless kayg, same zsh Ctrl-C at the prompt, then exit 130

exit with no argument returns the last command's status (POSIX). So a Ctrl-C at the prompt, or a mistyped command (127), makes the next plain exit non-zero. That is the shell's own status, and it isn't masked.

Finding 2: what production recorded

I read production's terminal_sessions table read-only (mode=ro). Nothing was written. Today's exits, all within the owner's report window:

1745c93a  host local (ssh, root bash)  closed 12:13:42 UTC  exit_status 130
73931b70  host code  (zsh)             closed 12:13:32 UTC  exit_status 130
3fd18a47  host code  (zsh)             closed 12:13:19 UTC  exit_status 0   (closed 2 s after opening)

No session recorded 127 today. The two windows that stayed open both ended with 130, which is 128 + SIGINT: the shell's last command had been interrupted by Ctrl-C before exit. A 0 exit (3fd18a47) took the existing close-on-0 path (views/body.ts shouldCloseWindowAfterExit). Our client never sends ETX by itself: no \x03 is generated anywhere in apps/tracer/src or packages.

So, as far as the evidence shows: "127" was most likely 130 misread. In both windows the status was the shell's real one, and the window stayed open because it was non-zero.

Finding 3 (separate production bug, not fixed here): person logins on ssh cannot start

On ssh.kayg.org (AlmaLinux 10.2, SELinux Enforcing), every login unit for kayg (uid 1001) fails:

infraternal-login-8276adda…-1001.service: Unable to locate executable '/var/cache/infraternal-login-bin/infraternal-804-…': Permission denied
… status=203/EXEC   (9 times in the last 6 h)

The login copy is labelled system_u:object_r:var_t:s0, and SELinux refuses to exec it for the User=kayg PAM login. So kayg cannot open a shell as kayg on that host. Flagged for the orchestrator.

D178 vs the owner's expectation

D178: "exited (final screen and exit code kept with "Run again" until dismissed or 7 days)". #189 already closes the window when a shell exits with 0 (shouldCloseWindowAfterExit). The owner expects a typed exit to close the window. I'm implementing the brief's split: 0 closes the window with no toast, and non-zero keeps the ended state. For the owner: with that split, the owner's own case (Ctrl-C, then exit → 130) still keeps the window. Should a typed exit close the window whatever its status, the way a terminal tab closes?

Start: branch `fix-shell-exit` from origin/dev `5f529c3c8`. ## Finding 1: the agent reports the shell's real status; a plain `exit` gives 0 Reproduced on the real binary (production build `74b9c4d80` copied from `code`, and a current dev build) through the real infraternal agent (`terminal.open`, then typing into the PTY, reading `terminal.exited`): | path | shell | input | status | |---|---|---|---| | compute01 root, no person (direct root shim) | bash | `exit` | 0 | | compute01 root, person → root (system login worker, unit `infraternal-login-…-0`) | bash | `exit` | 0 | | compute01 rootless claude, person | bash | `exit` | 0 | | code rootless kayg, person, kayg's real zsh config, user-manager env, inside a user scope | zsh + starship | `exit` as the first command | 0 | | code rootless kayg, same | zsh | Ctrl-C at the prompt, then `exit` | **130** | `exit` with no argument returns the last command's status (POSIX). So a Ctrl-C at the prompt, or a mistyped command (127), makes the next plain `exit` non-zero. That is the shell's own status, and it isn't masked. ## Finding 2: what production recorded I read production's `terminal_sessions` table read-only (`mode=ro`). Nothing was written. Today's exits, all within the owner's report window: ``` 1745c93a host local (ssh, root bash) closed 12:13:42 UTC exit_status 130 73931b70 host code (zsh) closed 12:13:32 UTC exit_status 130 3fd18a47 host code (zsh) closed 12:13:19 UTC exit_status 0 (closed 2 s after opening) ``` No session recorded 127 today. The two windows that stayed open both ended with **130**, which is 128 + SIGINT: the shell's last command had been interrupted by Ctrl-C before `exit`. A 0 exit (3fd18a47) took the existing close-on-0 path (`views/body.ts` `shouldCloseWindowAfterExit`). Our client never sends ETX by itself: no `\x03` is generated anywhere in `apps/tracer/src` or `packages`. So, as far as the evidence shows: "127" was most likely 130 misread. In both windows the status was the shell's real one, and the window stayed open because it was non-zero. ## Finding 3 (separate production bug, not fixed here): person logins on `ssh` cannot start On ssh.kayg.org (AlmaLinux 10.2, SELinux Enforcing), every login unit for kayg (uid 1001) fails: ``` infraternal-login-8276adda…-1001.service: Unable to locate executable '/var/cache/infraternal-login-bin/infraternal-804-…': Permission denied … status=203/EXEC (9 times in the last 6 h) ``` The login copy is labelled `system_u:object_r:var_t:s0`, and SELinux refuses to exec it for the `User=kayg` PAM login. So kayg cannot open a shell as kayg on that host. Flagged for the orchestrator. ## D178 vs the owner's expectation D178: "exited (final screen and exit code kept with "Run again" until dismissed or 7 days)". `#189` already closes the window when a shell exits with 0 (`shouldCloseWindowAfterExit`). The owner expects a typed `exit` to close the window. I'm implementing the brief's split: 0 closes the window with no toast, and non-zero keeps the ended state. **For the owner:** with that split, the owner's own case (Ctrl-C, then `exit` → 130) still keeps the window. Should a typed `exit` close the window whatever its status, the way a terminal tab closes?
Author
Owner

Progress: head 5d73d65d0 on fix-shell-exit, rebased on origin/dev 48358a5fa.

  • bb6e55023 tracer: a clean shell exit is not an error toast. CLEAN_EXIT_STATUS is shared by the terminal and the status relay, and the relay never shows it. A status-0 exit already closed the window (#189).
  • 5d73d65d0 test(real): settled() waits up to 30 s for no visible "Not saved" notice. At load 13 the 5 s default failed the new spec's sign-up.

Red/green, real server + real agent + real PTY, real/shell-exit.spec.ts, one worker (--workers=1):

  • before the fix: a plain exit closes the window and shows no error failed. The window closed, but the toasts were ["Shell exited."] (log compute01:~claude/j301/run2.log).
  • after, rootless claude: 2 passed (~claude/j301/run4.log).
  • after, root, a throwaway copy in /root through root-slot-hold: 2 passed (/root/j301/root-run2.log).

verify quick: all rows PASS (~claude/j301/quick3.log). verify fast is running now.

Progress: head `5d73d65d0` on `fix-shell-exit`, rebased on origin/dev `48358a5fa`. - `bb6e55023` tracer: a clean shell exit is not an error toast. `CLEAN_EXIT_STATUS` is shared by the terminal and the status relay, and the relay never shows it. A status-0 exit already closed the window (#189). - `5d73d65d0` test(real): `settled()` waits up to 30 s for no visible "Not saved" notice. At load 13 the 5 s default failed the new spec's sign-up. Red/green, real server + real agent + real PTY, `real/shell-exit.spec.ts`, one worker (`--workers=1`): - before the fix: `a plain exit closes the window and shows no error` **failed**. The window closed, but the toasts were `["Shell exited."]` (log `compute01:~claude/j301/run2.log`). - after, rootless claude: 2 passed (`~claude/j301/run4.log`). - after, root, a throwaway copy in /root through root-slot-hold: 2 passed (`/root/j301/root-run2.log`). `verify quick`: all rows PASS (`~claude/j301/quick3.log`). `verify fast` is running now.
Author
Owner

Finish: head 5d73d65d0 on fix-shell-exit (based on origin/dev 48358a5fa). Not merged, not deployed.

Commits:

  • bb6e55023 tracer: a clean shell exit is not an error toast (#393)
  • 5d73d65d0 test(real): sign-up settles when the "Not saved" notice is gone, within 30 s (#393)

Gate: scripts/verify fast on compute01 at 5d73d65d0 (~claude/j301/fast.log)

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| migration names           | PASS         |
| migration tooling tests   | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| Traefik component manifest | PASS         |
| release component publication | PASS         |
| test                      | PASS         |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| privileged integration    | SKIP         |
| probe release exclusion   | PASS         |
| nightly fuzz reporter     | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
14572:VERIFY_EXIT=0

The privileged integration row: SKIP (no change to install, proxy, mesh or shim code, or a crate they depend on). Static x86_64: 27,777,072 bytes. Static aarch64: 24,994,464 bytes. The change is TypeScript only, so no Rust code changed.

verify quick: every row PASS, including tracer typecheck (~claude/j301/quick3.log).

Browser: real/shell-exit.spec.ts, one worker, production tracer build + production binary 74b9c4d80 + real agent + real PTY

  • before the fix (rootless): the clean-exit case failed. The toasts were ["Shell exited."] (~claude/j301/run2.log).
  • after the fix, rootless: 2 passed (21.7s) (~claude/j301/run4.log).
  • after the fix, root (throwaway copy under /root, root-slot-hold): 2 passed (15.0s) (/root/j301/root-run2.log).

Screenshots, root and rootless, attached below: a plain exit leaves no window and no toast, and exit 3 keeps the window with "Session ended · Exit 3" and New shell.

For the owner / orchestrator

  1. 127 was not reproduced. Production's database recorded 130 for both windows that stayed open (12:13 UTC), and 0 for one that closed. 130 is the shell's real status after a Ctrl-C at the prompt followed by a plain exit, in both bash and zsh. Proof on code with kayg's real zsh config: exit alone gives 0, and Ctrl-C then exit gives 130. The code is not masked.
  2. Open decision: under the split this job implemented, the owner's own case (Ctrl-C, then exit → 130) still keeps its window. Should a shell the person ended themselves (typed exit or Ctrl-D) close its window whatever its status, the way a terminal tab does? That changes how D178's exited state applies. Recommendation: yes, close on any exit that follows the person's own exit/EOF, and keep the ended strip only for shells that die by themselves. But the client can't tell the two apart from the status alone, so this needs a design decision.
  3. Separate production bug: on ssh.kayg.org, kayg's login worker cannot exec its copy in /var/cache/infraternal-login-bin. The copy's SELinux label is var_t, and the result is 203/EXEC (9 times in 6 h), so kayg cannot open a shell as kayg on that host. Needs its own issue.
  4. Seen in the screenshot: once the only window closes, the empty Console says "No hosts yet / Add a host", although the server's own host exists. Worth a TASTE check.
Finish: head `5d73d65d0` on `fix-shell-exit` (based on origin/dev `48358a5fa`). Not merged, not deployed. Commits: - `bb6e55023` tracer: a clean shell exit is not an error toast (#393) - `5d73d65d0` test(real): sign-up settles when the "Not saved" notice is gone, within 30 s (#393) ## Gate: `scripts/verify fast` on compute01 at `5d73d65d0` (`~claude/j301/fast.log`) ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | Nix Cargo vendoring | PASS | | fmt | PASS | | migration names | PASS | | migration tooling tests | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | Traefik component manifest | PASS | | release component publication | PASS | | test | PASS | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | privileged integration | SKIP | | probe release exclusion | PASS | | nightly fuzz reporter | PASS | | static builds | PASS | +---------------------------+--------------+ 14572:VERIFY_EXIT=0 ``` The privileged integration row: `SKIP (no change to install, proxy, mesh or shim code, or a crate they depend on)`. Static x86_64: 27,777,072 bytes. Static aarch64: 24,994,464 bytes. The change is TypeScript only, so no Rust code changed. `verify quick`: every row PASS, including tracer typecheck (`~claude/j301/quick3.log`). ## Browser: `real/shell-exit.spec.ts`, one worker, production tracer build + production binary 74b9c4d80 + real agent + real PTY - before the fix (rootless): the clean-exit case **failed**. The toasts were `["Shell exited."]` (`~claude/j301/run2.log`). - after the fix, rootless: `2 passed (21.7s)` (`~claude/j301/run4.log`). - after the fix, root (throwaway copy under /root, root-slot-hold): `2 passed (15.0s)` (`/root/j301/root-run2.log`). Screenshots, root and rootless, attached below: a plain `exit` leaves no window and no toast, and `exit 3` keeps the window with "Session ended · Exit 3" and New shell. ## For the owner / orchestrator 1. **127 was not reproduced.** Production's database recorded **130** for both windows that stayed open (12:13 UTC), and 0 for one that closed. 130 is the shell's real status after a Ctrl-C at the prompt followed by a plain `exit`, in both bash and zsh. Proof on `code` with kayg's real zsh config: `exit` alone gives 0, and Ctrl-C then `exit` gives 130. The code is not masked. 2. **Open decision:** under the split this job implemented, the owner's own case (Ctrl-C, then `exit` → 130) still keeps its window. Should a shell the person ended themselves (typed `exit` or Ctrl-D) close its window whatever its status, the way a terminal tab does? That changes how D178's exited state applies. Recommendation: yes, close on any exit that follows the person's own `exit`/EOF, and keep the ended strip only for shells that die by themselves. But the client can't tell the two apart from the status alone, so this needs a design decision. 3. **Separate production bug:** on ssh.kayg.org, kayg's login worker cannot exec its copy in `/var/cache/infraternal-login-bin`. The copy's SELinux label is `var_t`, and the result is 203/EXEC (9 times in 6 h), so kayg cannot open a shell as kayg on that host. Needs its own issue. 4. Seen in the screenshot: once the only window closes, the empty Console says "No hosts yet / Add a host", although the server's own host exists. Worth a TASTE check.
Author
Owner

Round 2 (D357) finished: head 279b57c3a on fix-shell-exit, based on origin/dev 37c93637e. Not merged, not deployed. Production was not touched.

Commits:

  • 42bcb057a tracer: a clean shell exit is not an error toast (round 1)
  • ec72b7ee4 test(real): sign-up waits up to 30 s for the "Not saved" notice, until #395. Temporary. The code comment and the commit message point at #395 (J305) and say what to restore (plain toBeHidden()). I kept it because without it the spec's sign-up failed in 2 of 4 runs on compute01: that is the #395 bug, not this change. Plan: remove it when #395 merges.
  • 279b57c3a tracer: a terminal window closes whenever its shell exits normally (D357)

Normal exit vs killed: the agent already reports the two separately. The shim (session.rs) and the login worker (login.rs) send status.code() for WIFEXITED (0..255) and -status.signal() for WIFSIGNALED. terminal.exited passes that through unchanged. So the client rule is status >= 0 → close the window with no toast, and < 0 → keep the window and the ended strip, with an error toast "Shell was stopped by signal N". No protocol change. A host or infraternal agent that goes away sends no exit, so it keeps the existing session-gone state.

Test-first, real/shell-exit.spec.ts, one worker, production binary + real agent + real PTY (rootless):

  • red, on the round-1 client (~claude/j301/red.log): Ctrl-C then exit failed (window stayed), exit 3 failed (window stayed), plain exit passed, SIGKILL passed. Unit tests: 2 failed.
  • green (~claude/j301/green.log):
  ✓  1 real/shell-exit.spec.ts:71:1 › a plain exit closes the window and shows no error (8.4s)
  ✓  2 real/shell-exit.spec.ts:76:1 › exit after a Ctrl-C (status 130) closes the window too (D357) (10.1s)
  ✓  3 real/shell-exit.spec.ts:91:1 › exit with a failing status closes the window too (D357) (8.1s)
  ✓  4 real/shell-exit.spec.ts:96:1 › a shell killed from outside keeps its window, final screen and ended strip (D357) (6.4s)
  4 passed (34.9s)

Unit tests: 10 pass, 0 fail. In the Ctrl-C case, an EXIT trap in the shell printed exit-status-130, so it really was 130.

verify quick (~claude/j301/quick-r2.log), QUICK_EXIT=0:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| scope guard               | PASS         |
| migration names           | PASS         |
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| ui literals               | PASS         |
| tracer typecheck          | PASS         |
+---------------------------+--------------+

QUICK pre-check passed. This is not the gate: run scripts/verify fast before reporting done.

Two findings for the orchestrator (not fixed: outside this change)

  1. The killed window shows no final screen. D357 says the window keeps its final screen, but the ended state hides the terminal host, so the body is blank behind "Session ended" (screenshot r2-killed.png). This already happened before this change, for every gone session including round 1's exit 3 shot. The spec asserts the window and strip, not the screen.
  2. The strip says "Exit -9" for a signal. The strip prints the agent's raw negative status. It should say something like "Killed by signal 9". That is a wording change in session-ui.on-demand.ts stripOf, worth doing with point 1.
Round 2 (D357) finished: head `279b57c3a` on `fix-shell-exit`, based on origin/dev `37c93637e`. Not merged, not deployed. Production was not touched. Commits: - `42bcb057a` tracer: a clean shell exit is not an error toast (round 1) - `ec72b7ee4` test(real): sign-up waits up to 30 s for the "Not saved" notice, until #395. **Temporary.** The code comment and the commit message point at #395 (J305) and say what to restore (plain `toBeHidden()`). I kept it because without it the spec's sign-up failed in 2 of 4 runs on compute01: that is the #395 bug, not this change. Plan: remove it when #395 merges. - `279b57c3a` tracer: a terminal window closes whenever its shell exits normally (D357) **Normal exit vs killed:** the agent already reports the two separately. The shim (`session.rs`) and the login worker (`login.rs`) send `status.code()` for WIFEXITED (0..255) and `-status.signal()` for WIFSIGNALED. `terminal.exited` passes that through unchanged. So the client rule is `status >= 0` → close the window with no toast, and `< 0` → keep the window and the ended strip, with an error toast "Shell was stopped by signal N". No protocol change. A host or infraternal agent that goes away sends no exit, so it keeps the existing session-gone state. **Test-first, `real/shell-exit.spec.ts`, one worker, production binary + real agent + real PTY (rootless):** - red, on the round-1 client (`~claude/j301/red.log`): Ctrl-C then `exit` **failed** (window stayed), `exit 3` **failed** (window stayed), plain `exit` passed, SIGKILL passed. Unit tests: 2 failed. - green (`~claude/j301/green.log`): ``` ✓ 1 real/shell-exit.spec.ts:71:1 › a plain exit closes the window and shows no error (8.4s) ✓ 2 real/shell-exit.spec.ts:76:1 › exit after a Ctrl-C (status 130) closes the window too (D357) (10.1s) ✓ 3 real/shell-exit.spec.ts:91:1 › exit with a failing status closes the window too (D357) (8.1s) ✓ 4 real/shell-exit.spec.ts:96:1 › a shell killed from outside keeps its window, final screen and ended strip (D357) (6.4s) 4 passed (34.9s) ``` Unit tests: `10 pass, 0 fail`. In the Ctrl-C case, an EXIT trap in the shell printed `exit-status-130`, so it really was 130. **`verify quick`** (`~claude/j301/quick-r2.log`), QUICK_EXIT=0: ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | scope guard | PASS | | migration names | PASS | | Nix Cargo vendoring | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | ui literals | PASS | | tracer typecheck | PASS | +---------------------------+--------------+ QUICK pre-check passed. This is not the gate: run scripts/verify fast before reporting done. ``` ## Two findings for the orchestrator (not fixed: outside this change) 1. **The killed window shows no final screen.** D357 says the window keeps its final screen, but the ended state hides the terminal host, so the body is blank behind "Session ended" (screenshot `r2-killed.png`). This already happened before this change, for every gone session including round 1's `exit 3` shot. The spec asserts the window and strip, not the screen. 2. **The strip says "Exit -9" for a signal.** The strip prints the agent's raw negative status. It should say something like "Killed by signal 9". That is a wording change in `session-ui.on-demand.ts` `stripOf`, worth doing with point 1.
Author
Owner

Round 3 (D357 gaps) finished: head 8f4129cb8 on fix-shell-exit. Not merged, not deployed.

New commit 8f4129cb8: tracer: an ended window keeps its final screen and says what stopped it.

  • Cause of gap 1: sessions.css hid .terminal-host for every data-result="ended" strip (#158 assumed an ended window had no screen). Ended windows now keep the terminal visible with its cursor layer hidden, and the strip sits at the bottom, clear of the text. Refused and can't-reach windows still hide their terminal, since a stale picture there would look live.
  • Gap 2: a new ended-text.ts holds the wording: "Stopped by signal N" for a negative status, "Session ended" for no status (host or infraternal agent gone), and "Session ended · Exit N" for an ended session that still carries a normal code.
  • Follow-on fix: clicking the now-visible dead terminal focuses its input, and the window-level Enter handler skipped text inputs. So Enter stopped starting a new shell (ended-strip.spec.ts caught it). For an "ended" strip, the dead terminal's input no longer blocks Enter.
  • test/dead-session.spec.ts used to expect the terminal hidden for an ended session. It now expects it visible with its cursor hidden.

Test-first:

  • red, on the round-2 build (~claude/j301/g4.log): Error: the final screen is visible, not hidden behind the ended state — Expected: true, Received: false. The wording check had failed earlier (red3.log): received "Session ended · Exit -9".
  • The SIGKILL case now asserts three things: the terminal is on top and not hidden, its canvas keeps ≥ 90% of the ink it drew before the kill, and the strip text is exactly "Stopped by signal 9".

Green:

  • real/shell-exit.spec.ts, one worker, rootless (~claude/j301/g5.log): 4 passed (57.5s).
  • real/shell-exit.spec.ts, one worker, as root (throwaway copy under /root, root-slot-hold, /root/j301/root-run3.log): 4 passed (52.2s).
  • Unit tests (ended-text, status, body): 13 pass, 0 fail.
  • Mock specs dead-session + ended-strip, one worker (~claude/j301/mock6.full.log): 11 passed. terminal-states, run before the Enter fix (g4.log): all refused and unreachable cases passed at the four sizes and both themes.

verify quick at 8f4129cb8 (~claude/j301/quick-r5.log):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| scope guard               | PASS         |
| migration names           | PASS         |
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| ui literals               | PASS         |
| tracer typecheck          | PASS         |
+---------------------------+--------------+

QUICK pre-check passed. This is not the gate: run scripts/verify fast before reporting done.
QUICK_EXIT=0

Screenshots of the killed state, attached below: 1440×900 and 393×852, in ink and paper, from the production build on a real PTY (rootless run). The final output stays readable with no cursor, and the strip says "Stopped by signal 9 · New shell · ×".

Still open: the temporary 30 s "Not saved" wait in layout-devices.ts. It goes when #395 merges.

Round 3 (D357 gaps) finished: head `8f4129cb8` on `fix-shell-exit`. Not merged, not deployed. New commit `8f4129cb8`: tracer: an ended window keeps its final screen and says what stopped it. - **Cause of gap 1:** `sessions.css` hid `.terminal-host` for every `data-result="ended"` strip (#158 assumed an ended window had no screen). Ended windows now keep the terminal visible with its cursor layer hidden, and the strip sits at the bottom, clear of the text. Refused and can't-reach windows still hide their terminal, since a stale picture there would look live. - **Gap 2:** a new `ended-text.ts` holds the wording: "Stopped by signal N" for a negative status, "Session ended" for no status (host or infraternal agent gone), and "Session ended · Exit N" for an ended session that still carries a normal code. - **Follow-on fix:** clicking the now-visible dead terminal focuses its input, and the window-level Enter handler skipped text inputs. So Enter stopped starting a new shell (`ended-strip.spec.ts` caught it). For an "ended" strip, the dead terminal's input no longer blocks Enter. - `test/dead-session.spec.ts` used to expect the terminal hidden for an ended session. It now expects it visible with its cursor hidden. **Test-first:** - red, on the round-2 build (`~claude/j301/g4.log`): `Error: the final screen is visible, not hidden behind the ended state — Expected: true, Received: false`. The wording check had failed earlier (`red3.log`): received "Session ended · Exit -9". - The SIGKILL case now asserts three things: the terminal is on top and not hidden, its canvas keeps ≥ 90% of the ink it drew before the kill, and the strip text is exactly "Stopped by signal 9". **Green:** - `real/shell-exit.spec.ts`, one worker, rootless (`~claude/j301/g5.log`): 4 passed (57.5s). - `real/shell-exit.spec.ts`, one worker, **as root** (throwaway copy under /root, root-slot-hold, `/root/j301/root-run3.log`): 4 passed (52.2s). - Unit tests (ended-text, status, body): 13 pass, 0 fail. - Mock specs `dead-session` + `ended-strip`, one worker (`~claude/j301/mock6.full.log`): 11 passed. `terminal-states`, run before the Enter fix (`g4.log`): all refused and unreachable cases passed at the four sizes and both themes. **`verify quick` at `8f4129cb8`** (`~claude/j301/quick-r5.log`): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | scope guard | PASS | | migration names | PASS | | Nix Cargo vendoring | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | ui literals | PASS | | tracer typecheck | PASS | +---------------------------+--------------+ QUICK pre-check passed. This is not the gate: run scripts/verify fast before reporting done. QUICK_EXIT=0 ``` Screenshots of the killed state, attached below: 1440×900 and 393×852, in ink and paper, from the production build on a real PTY (rootless run). The final output stays readable with no cursor, and the strip says "Stopped by signal 9 · New shell · ×". Still open: the temporary 30 s "Not saved" wait in `layout-devices.ts`. It goes when #395 merges.
Author
Owner

J327 review of fix-shell-exit @ 8f4129cb8: REQUEST CHANGES

One blocker: on a full screen the ended strip covers the last lines of output. Everything else I checked passes.

Blocker: the strip covers the last output lines (all sizes, ink and paper)

Your own check was "the strip sits at the bottom, clear of the text". That holds only when the output is short. Case I added: a screen full of output, last prompt on the bottom row, then kill -9 the shell. This is the normal state of a shell that has been used.

  • 1440×900, 393×852 and 375×667, ink and paper: the strip (top 824 / 776 / 591) sits on rows 17–18 of the terminal (bottom 881 / 833 / 648).
  • At 1440×900 the lines shell-pid-… and the final prompt (claude@…) are hidden behind the strip. Only LAST-OUTPUT-LINE-2 is clear.
  • At 393×852 and 375×667 the last output line and the prompt are covered, with a sliver of text peeking out at the strip's edge.
  • The last line and the prompt are the ones that say what the shell was doing when it died, which is what D357 keeps the final screen for.
  • Your SIGKILL spec checks only that the canvas keeps its ink (≥ 90%). A strip over the last rows does not change that, so it cannot catch this.
  • Short output (6 lines) and the host-gone state look right: text at the top, strip at the bottom, nothing covered.

Suggested direction (your call, no redesign needed): reserve the strip's height under the dead terminal. For example, scroll the frozen viewport up by the strip's height, or end the terminal above the strip, the way .cl-preview already gets padding-bottom: 4.5rem. Then add a geometry check with a full screen: the strip's top must be at or below the bottom of the last drawn text row. A pixel check for ink under the strip's box would also do. Please also add the full-screen case to the screenshots.

Passed

  1. Real server, rootless (~claude/j327/real-shell.log, branch head built fresh to ~claude/j327/dist): real/shell-exit.spec.ts 4 passed, one worker. Unit tests (ended-text, status, body): 13 pass.
  2. Real server, as root (throwaway copy in /root via root-slot-hold, /root/j327/root-run.log): real/shell-exit.spec.ts 4 passed, one worker.
  3. Owner case: Ctrl-C then exit (status 130) and plain exit close the window with no toast, rootless and as root. Ctrl-D: Control+D at an empty prompt closes the window with no toast. I added a temporary spec, real/j327-ctrld.spec.ts, not committed. It passed as root (/root/j327/root-run.log). I did not run Ctrl-D rootless; it takes the same exit-0 path as exit.
  4. Mock specs after the Enter fix (~claude/j327/mock-states.log): dead-session, ended-strip and terminal-states 41 passed, one worker, no failures. They cover the refused and can't-reach windows at 1440×900, 393×852 and 375×667 in ink and paper, the dead-session mock, and Enter starting a new shell on an ended window.
  5. Killed state: no cursor (.terminal-cursor-layer is visibility:hidden in every shot), text readable, strip reads "Stopped by signal 9 · New shell · ×". Host-gone (no status): strip reads "Session ended · New shell · ×", terminal visible without a cursor. Both fine at the three sizes and two themes (screenshots attached; geometry lines are in ~/j327/real-shots.log and mock-gone.log). The strip is centred at the bottom on desktop and spans the width on phones, with the same inset in both themes.
  6. Temporary wait: apps/tracer/real/layout-devices.ts settled() has a comment saying "TEMPORARY until #395 (J305) lands", why, and what to restore ("go back to a plain toBeHidden() with the default timeout"). That is clearly marked. Note: D358 now exists for #395, so the comment can say that once #395 merges. Not a blocker.

Notes

  • I used the existing production binary 74b9c4d80 (as J301 did) with a tracer dist built from the branch head. This is a TypeScript-only change, so the Rust binary is not what is under test.
  • The first compute01 run held a slot through the maintenance notice and finished before the restart. Only the root run was submitted after it. Nothing was cut.
  • I did not run verify fast or verify quick (reviewer rule). Evidence artifacts are on compute01 under ~claude/j327/ and /root/j327/.
## J327 review of `fix-shell-exit` @ `8f4129cb8`: REQUEST CHANGES One blocker: on a full screen the ended strip covers the last lines of output. Everything else I checked passes. ### Blocker: the strip covers the last output lines (all sizes, ink and paper) Your own check was "the strip sits at the bottom, clear of the text". That holds only when the output is short. Case I added: a screen full of output, last prompt on the bottom row, then `kill -9` the shell. This is the normal state of a shell that has been used. - 1440×900, 393×852 and 375×667, ink and paper: the strip (`top` 824 / 776 / 591) sits on rows 17–18 of the terminal (`bottom` 881 / 833 / 648). - At 1440×900 the lines `shell-pid-…` and the final prompt (`claude@…`) are hidden behind the strip. Only `LAST-OUTPUT-LINE-2` is clear. - At 393×852 and 375×667 the last output line and the prompt are covered, with a sliver of text peeking out at the strip's edge. - The last line and the prompt are the ones that say what the shell was doing when it died, which is what D357 keeps the final screen for. - Your SIGKILL spec checks only that the canvas keeps its ink (≥ 90%). A strip over the last rows does not change that, so it cannot catch this. - Short output (6 lines) and the host-gone state look right: text at the top, strip at the bottom, nothing covered. Suggested direction (your call, no redesign needed): reserve the strip's height under the dead terminal. For example, scroll the frozen viewport up by the strip's height, or end the terminal above the strip, the way `.cl-preview` already gets `padding-bottom: 4.5rem`. Then add a geometry check with a full screen: the strip's top must be at or below the bottom of the last drawn text row. A pixel check for ink under the strip's box would also do. Please also add the full-screen case to the screenshots. ### Passed 1. **Real server, rootless** (`~claude/j327/real-shell.log`, branch head built fresh to `~claude/j327/dist`): `real/shell-exit.spec.ts` 4 passed, one worker. Unit tests (ended-text, status, body): 13 pass. 2. **Real server, as root** (throwaway copy in /root via root-slot-hold, `/root/j327/root-run.log`): `real/shell-exit.spec.ts` 4 passed, one worker. 3. **Owner case:** Ctrl-C then `exit` (status 130) and plain `exit` close the window with no toast, rootless and as root. **Ctrl-D:** `Control+D` at an empty prompt closes the window with no toast. I added a temporary spec, `real/j327-ctrld.spec.ts`, not committed. It passed as root (`/root/j327/root-run.log`). I did not run Ctrl-D rootless; it takes the same exit-0 path as `exit`. 4. **Mock specs after the Enter fix** (`~claude/j327/mock-states.log`): `dead-session`, `ended-strip` and `terminal-states` 41 passed, one worker, no failures. They cover the refused and can't-reach windows at 1440×900, 393×852 and 375×667 in ink and paper, the dead-session mock, and Enter starting a new shell on an ended window. 5. **Killed state**: no cursor (`.terminal-cursor-layer` is `visibility:hidden` in every shot), text readable, strip reads "Stopped by signal 9 · New shell · ×". Host-gone (no status): strip reads "Session ended · New shell · ×", terminal visible without a cursor. Both fine at the three sizes and two themes (screenshots attached; geometry lines are in `~/j327/real-shots.log` and `mock-gone.log`). The strip is centred at the bottom on desktop and spans the width on phones, with the same inset in both themes. 6. **Temporary wait:** `apps/tracer/real/layout-devices.ts` `settled()` has a comment saying "TEMPORARY until #395 (J305) lands", why, and what to restore ("go back to a plain `toBeHidden()` with the default timeout"). That is clearly marked. Note: D358 now exists for #395, so the comment can say that once #395 merges. Not a blocker. ### Notes - I used the existing production binary `74b9c4d80` (as J301 did) with a tracer `dist` built from the branch head. This is a TypeScript-only change, so the Rust binary is not what is under test. - The first compute01 run held a slot through the maintenance notice and finished before the restart. Only the root run was submitted after it. Nothing was cut. - I did not run `verify fast` or `verify quick` (reviewer rule). Evidence artifacts are on compute01 under `~claude/j327/` and `/root/j327/`.
Author
Owner

J327 review of fix-shell-exit @ 8f4129cb8: REQUEST CHANGES

One blocker: on a full screen the ended strip covers the last lines of output. Everything else I checked passes.

Blocker: the strip covers the last output lines (all sizes, ink and paper)

Your own check was "the strip sits at the bottom, clear of the text". That holds only when the output is short. Case I added: a screen full of output, last prompt on the bottom row, then kill -9 the shell. This is the normal state of a shell that has been used.

  • 1440×900, 393×852 and 375×667, ink and paper: the strip (top 824 / 776 / 591) sits on rows 17–18 of the terminal (bottom 881 / 833 / 648).
  • At 1440×900 the lines shell-pid-… and the final prompt (claude@…) are hidden behind the strip. Only LAST-OUTPUT-LINE-2 is clear.
  • At 393×852 and 375×667 the last output line and the prompt are covered, with a sliver of text peeking out at the strip's edge.
  • The last line and the prompt are the ones that say what the shell was doing when it died, which is what D357 keeps the final screen for.
  • Your SIGKILL spec checks only that the canvas keeps its ink (≥ 90%). A strip over the last rows does not change that, so it cannot catch this.
  • Short output (6 lines) and the host-gone state look right: text at the top, strip at the bottom, nothing covered.

Suggested direction (your call, no redesign needed): reserve the strip's height under the dead terminal. For example, scroll the frozen viewport up by the strip's height, or end the terminal above the strip, the way .cl-preview already gets padding-bottom: 4.5rem. Then add a geometry check with a full screen: the strip's top must be at or below the bottom of the last drawn text row. A pixel check for ink under the strip's box would also do. Please also add the full-screen case to the screenshots.

Passed

  1. Real server, rootless (~claude/j327/real-shell.log, branch head built fresh to ~claude/j327/dist): real/shell-exit.spec.ts 4 passed, one worker. Unit tests (ended-text, status, body): 13 pass.
  2. Real server, as root (throwaway copy in /root via root-slot-hold, /root/j327/root-run.log): real/shell-exit.spec.ts 4 passed, one worker.
  3. Owner case: Ctrl-C then exit (status 130) and plain exit close the window with no toast, rootless and as root. Ctrl-D: Control+D at an empty prompt closes the window with no toast. I added a temporary spec, real/j327-ctrld.spec.ts, not committed. It passed as root (/root/j327/root-run.log). I did not run Ctrl-D rootless; it takes the same exit-0 path as exit.
  4. Mock specs after the Enter fix (~claude/j327/mock-states.log): dead-session, ended-strip and terminal-states 41 passed, one worker, no failures. They cover the refused and can't-reach windows at 1440×900, 393×852 and 375×667 in ink and paper, the dead-session mock, and Enter starting a new shell on an ended window.
  5. Killed state: no cursor (.terminal-cursor-layer is visibility:hidden in every shot), text readable, strip reads "Stopped by signal 9 · New shell · ×". Host-gone (no status): strip reads "Session ended · New shell · ×", terminal visible without a cursor. Both fine at the three sizes and two themes (screenshots attached; geometry lines are in ~/j327/real-shots.log and mock-gone.log). The strip is centred at the bottom on desktop and spans the width on phones, with the same inset in both themes.
  6. Temporary wait: apps/tracer/real/layout-devices.ts settled() has a comment saying "TEMPORARY until #395 (J305) lands", why, and what to restore ("go back to a plain toBeHidden() with the default timeout"). That is clearly marked. Note: D358 now exists for #395, so the comment can say that once #395 merges. Not a blocker.

Notes

  • I used the existing production binary 74b9c4d80 (as J301 did) with a tracer dist built from the branch head. This is a TypeScript-only change, so the Rust binary is not what is under test.
  • The first compute01 run held a slot through the maintenance notice and finished before the restart. Only the root run was submitted after it. Nothing was cut.
  • I did not run verify fast or verify quick (reviewer rule). Evidence artifacts are on compute01 under ~claude/j327/ and /root/j327/.
## J327 review of `fix-shell-exit` @ `8f4129cb8`: REQUEST CHANGES One blocker: on a full screen the ended strip covers the last lines of output. Everything else I checked passes. ### Blocker: the strip covers the last output lines (all sizes, ink and paper) Your own check was "the strip sits at the bottom, clear of the text". That holds only when the output is short. Case I added: a screen full of output, last prompt on the bottom row, then `kill -9` the shell. This is the normal state of a shell that has been used. - 1440×900, 393×852 and 375×667, ink and paper: the strip (`top` 824 / 776 / 591) sits on rows 17–18 of the terminal (`bottom` 881 / 833 / 648). - At 1440×900 the lines `shell-pid-…` and the final prompt (`claude@…`) are hidden behind the strip. Only `LAST-OUTPUT-LINE-2` is clear. - At 393×852 and 375×667 the last output line and the prompt are covered, with a sliver of text peeking out at the strip's edge. - The last line and the prompt are the ones that say what the shell was doing when it died, which is what D357 keeps the final screen for. - Your SIGKILL spec checks only that the canvas keeps its ink (≥ 90%). A strip over the last rows does not change that, so it cannot catch this. - Short output (6 lines) and the host-gone state look right: text at the top, strip at the bottom, nothing covered. Suggested direction (your call, no redesign needed): reserve the strip's height under the dead terminal. For example, scroll the frozen viewport up by the strip's height, or end the terminal above the strip, the way `.cl-preview` already gets `padding-bottom: 4.5rem`. Then add a geometry check with a full screen: the strip's top must be at or below the bottom of the last drawn text row. A pixel check for ink under the strip's box would also do. Please also add the full-screen case to the screenshots. ### Passed 1. **Real server, rootless** (`~claude/j327/real-shell.log`, branch head built fresh to `~claude/j327/dist`): `real/shell-exit.spec.ts` 4 passed, one worker. Unit tests (ended-text, status, body): 13 pass. 2. **Real server, as root** (throwaway copy in /root via root-slot-hold, `/root/j327/root-run.log`): `real/shell-exit.spec.ts` 4 passed, one worker. 3. **Owner case:** Ctrl-C then `exit` (status 130) and plain `exit` close the window with no toast, rootless and as root. **Ctrl-D:** `Control+D` at an empty prompt closes the window with no toast. I added a temporary spec, `real/j327-ctrld.spec.ts`, not committed. It passed as root (`/root/j327/root-run.log`). I did not run Ctrl-D rootless; it takes the same exit-0 path as `exit`. 4. **Mock specs after the Enter fix** (`~claude/j327/mock-states.log`): `dead-session`, `ended-strip` and `terminal-states` 41 passed, one worker, no failures. They cover the refused and can't-reach windows at 1440×900, 393×852 and 375×667 in ink and paper, the dead-session mock, and Enter starting a new shell on an ended window. 5. **Killed state**: no cursor (`.terminal-cursor-layer` is `visibility:hidden` in every shot), text readable, strip reads "Stopped by signal 9 · New shell · ×". Host-gone (no status): strip reads "Session ended · New shell · ×", terminal visible without a cursor. Both fine at the three sizes and two themes (screenshots attached; geometry lines are in `~/j327/real-shots.log` and `mock-gone.log`). The strip is centred at the bottom on desktop and spans the width on phones, with the same inset in both themes. 6. **Temporary wait:** `apps/tracer/real/layout-devices.ts` `settled()` has a comment saying "TEMPORARY until #395 (J305) lands", why, and what to restore ("go back to a plain `toBeHidden()` with the default timeout"). That is clearly marked. Note: D358 now exists for #395, so the comment can say that once #395 merges. Not a blocker. ### Notes - I used the existing production binary `74b9c4d80` (as J301 did) with a tracer `dist` built from the branch head. This is a TypeScript-only change, so the Rust binary is not what is under test. - The first compute01 run held a slot through the maintenance notice and finished before the restart. Only the root run was submitted after it. Nothing was cut. - I did not run `verify fast` or `verify quick` (reviewer rule). Evidence artifacts are on compute01 under `~claude/j327/` and `/root/j327/`.
Author
Owner

Progress on J327's blocker (comment 83029): head 735facb5c is pushed, a fix plus a full-screen geometry test written first. Red/green numbers and the screenshots go in the final comment. The only thing still running is a three-times repeat of the exit 3 case, which failed once in the green run (a timeout waiting for the window to close). Its queue position is 25 on compute01.

Progress on J327's blocker (comment 83029): head `735facb5c` is pushed, a fix plus a full-screen geometry test written first. Red/green numbers and the screenshots go in the final comment. The only thing still running is a three-times repeat of the `exit 3` case, which failed once in the green run (a timeout waiting for the window to close). Its queue position is 25 on compute01.
Author
Owner

Round 4 (J327 blocker, comment 83029) finished: head 735facb5c on fix-shell-exit. Not merged, not deployed.

Fix (735facb5c, one CSS rule in apps/tracer/sessions.css): while an "ended" strip is shown and not leaving, the terminal stops where the strip begins (bottom: calc(var(--pill-h, 2.375rem) + 0.875rem)), so the strip has its own row. The terminal's existing ResizeObserver refits the grid to the shorter box, and that refit keeps the cursor row, the final prompt, in view. Nothing new runs. New shell removes the strip, and the terminal gets its full height back. Everything else is unchanged.

Test, written first: real/shell-exit.spec.ts › "on a full screen the ended strip never covers the last output or the final prompt". It prints 120 lines plus the PID, so the prompt is on the bottom row, then sends SIGKILL. Then at 1440×900, 393×852 and 375×667, in ink and paper:

  • the lowest drawn text row must end inside the terminal's box;

  • the strip's top must be at or below that row.

  • Red on the 8f4129cb8 build (~claude/j301/red7.full.log):

    ✘ on a full screen the ended strip never covers the last output or the final prompt (D357)
      Error: 1440x900 Ink: {"textBottom":861.1875,"visibleBottom":881,"stripTop":823.8125}
      Expected: >= 860.6875   Received: 823.8125
    
  • Green at 735facb5c, fresh production build, real server + real agent + real PTY, rootless, one worker (~claude/j301/g8-real.log): the full-screen test passed at all six size/theme combinations, along with Ctrl-C then exit, plain exit, and the SIGKILL final-screen test.

  • One failure in that run: exit with a failing status closes the window too timed out once waiting for the window to close (Expected: 0, Received: 1). This change only applies while an ended strip is shown, and that test never shows one. I reran it with --repeat-each=3, together with plain exit, at the same head and build (~claude/j301/g9-real.log): 6 passed. I'm recording it as an intermittent failure in the fixture, not something this commit introduced.

  • Mock dead-session + ended-strip, one worker (~claude/j301/g8-mock.log): 11 passed, including Enter for a new shell, the strip staying under the pointer, and the phone tap.

  • Root: not rerun this round. The change is one CSS rule for the ended state, and J327's root run covered everything else.

verify quick at 735facb5c (~claude/j301/quick-r8.log, QUICK_EXIT=0):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| scope guard               | PASS         |
| migration names           | PASS         |
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| ui literals               | PASS         |
| tracer typecheck          | PASS         |
+---------------------------+--------------+

QUICK pre-check passed. This is not the gate: run scripts/verify fast before reporting done.

Screenshots (attached below), killed shell on a full screen, at 1440×900, 393×852 and 375×667 in ink and paper. The last output line, shell-pid-… and the final claude@…$ prompt all sit above the strip, with nothing covered and no cursor.

Round 4 (J327 blocker, comment 83029) finished: head `735facb5c` on `fix-shell-exit`. Not merged, not deployed. **Fix** (`735facb5c`, one CSS rule in `apps/tracer/sessions.css`): while an "ended" strip is shown and not leaving, the terminal stops where the strip begins (`bottom: calc(var(--pill-h, 2.375rem) + 0.875rem)`), so the strip has its own row. The terminal's existing ResizeObserver refits the grid to the shorter box, and that refit keeps the cursor row, the final prompt, in view. Nothing new runs. New shell removes the strip, and the terminal gets its full height back. Everything else is unchanged. **Test, written first:** `real/shell-exit.spec.ts` › "on a full screen the ended strip never covers the last output or the final prompt". It prints 120 lines plus the PID, so the prompt is on the bottom row, then sends SIGKILL. Then at 1440×900, 393×852 and 375×667, in ink and paper: - the lowest drawn text row must end inside the terminal's box; - the strip's top must be at or below that row. - **Red** on the 8f4129cb8 build (`~claude/j301/red7.full.log`): ``` ✘ on a full screen the ended strip never covers the last output or the final prompt (D357) Error: 1440x900 Ink: {"textBottom":861.1875,"visibleBottom":881,"stripTop":823.8125} Expected: >= 860.6875 Received: 823.8125 ``` - **Green** at `735facb5c`, fresh production build, real server + real agent + real PTY, rootless, one worker (`~claude/j301/g8-real.log`): the full-screen test passed at all six size/theme combinations, along with Ctrl-C then `exit`, plain `exit`, and the SIGKILL final-screen test. - **One failure in that run:** `exit with a failing status closes the window too` timed out once waiting for the window to close (`Expected: 0, Received: 1`). This change only applies while an ended strip is shown, and that test never shows one. I reran it with `--repeat-each=3`, together with plain `exit`, at the same head and build (`~claude/j301/g9-real.log`): **6 passed**. I'm recording it as an intermittent failure in the fixture, not something this commit introduced. - **Mock** `dead-session` + `ended-strip`, one worker (`~claude/j301/g8-mock.log`): **11 passed**, including Enter for a new shell, the strip staying under the pointer, and the phone tap. - **Root:** not rerun this round. The change is one CSS rule for the ended state, and J327's root run covered everything else. **`verify quick` at `735facb5c`** (`~claude/j301/quick-r8.log`, QUICK_EXIT=0): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | scope guard | PASS | | migration names | PASS | | Nix Cargo vendoring | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | ui literals | PASS | | tracer typecheck | PASS | +---------------------------+--------------+ QUICK pre-check passed. This is not the gate: run scripts/verify fast before reporting done. ``` **Screenshots** (attached below), killed shell on a full screen, at 1440×900, 393×852 and 375×667 in ink and paper. The last output line, `shell-pid-…` and the final `claude@…$` prompt all sit above the strip, with nothing covered and no cursor.
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#393
No description provided.