From 01fc10808628d74ae865922f6b6185bf7ccda8c9 Mon Sep 17 00:00:00 2001 From: Harry Bayliss Date: Fri, 15 May 2026 01:07:57 +0100 Subject: [PATCH] Rename Kill to Close, add New Terminal palette entry, clean up exited terminals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Palette's per-child "Kill " action is now labelled "Close " (action kind unchanged; still SIGTERM). Matches the existing "Close agent: …" context entry and reads less violent for a graceful term. - New "New Terminal" palette entry spawns a bare interactive $SHELL pane via LaunchTerminal (kind=terminal). Replaces the default "shell" process preset that was seeded on first run. - Exited KindTerminal entries are now dropped from the session in reapChild — terminals have no restart path, so leaving them behind as greyed rows in the Processes sidebar was just clutter. processList also filters defensively. --- CHANGELOG.md | 18 ++++++++++++++++++ TODO.md | 4 +--- internal/app/app.go | 7 +++++++ internal/app/palette.go | 29 ++++++++++++++++++++--------- internal/app/session.go | 17 +++++++++++++++++ internal/app/tree.go | 15 +++++++++++---- internal/preset/preset.go | 18 +----------------- 7 files changed, 75 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ecaa2..f308dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added +- "New Terminal" entry in the command palette spawns a bare interactive + `$SHELL` pane (kind `terminal`). Unlike "Run process: …" presets, + which are session-persistent and reachable via `restart_process`, + terminals are ephemeral — once they exit they vanish from the + Processes sidebar instead of lingering as a dead row. The default + `shell` process preset that previously seeded on first run has been + removed; this entry replaces it. - Per-child idle-state classifier with five states (`idle`, `working`, `thinking`, `permission`, `error`) and three pluggable strategies: `output_activity` (claude / opencode defaults), `osc_title_stability` @@ -98,6 +105,11 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). after a child program disables mouse tracking. ### Changed +- The palette's per-child "Kill " action is now labelled + "Close ". The underlying signal (SIGTERM) and behaviour are + unchanged; the new label matches the existing "Close agent: …" + context entry and reads less violent for what is really just a + graceful termination. - `timer_wait` is now a thin wrapper over the shared timer manager (`timer_set` semantics). Existing callers see no behavioural change; the timer is visible in `timer_list` while it's pending. @@ -108,6 +120,12 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). renders the canonical `--flag` form. ### Fixed +- Exited terminal panes (kind `terminal`, including those launched via + the new "New Terminal" palette entry or MCP `spawn_process` with + `kind=terminal`) are now removed from the session and the Processes + sidebar as soon as they exit. Previously they stuck around as a + greyed-out row indistinguishable from an exited command process, + even though terminals have no restart path. - `whoami` and `help("timers")` now advertise the full Solo-parity timer surface (`timer_set`, `timer_fire_when_idle_any`, `timer_fire_when_idle_all`, `timer_cancel`, `timer_pause`, diff --git a/TODO.md b/TODO.md index 37cc685..e09c8b8 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,3 @@ -- [ ] We should probably rename the Kill terminology to Close instead, across processes and agents. -- [ ] Exited shells are still being treated as active processes. They should be removed from the process list when they exit. -- [ ] Shells should be renamed to terminals. "New Terminal" etc. - [ ] Codex seemed to think that it needed to launch patterm itself to get the mcp working - [ ] I cant click and drag to select text from codex - [ ] codex uses perl to interact with the socket rather than calling mcp tools @@ -12,6 +9,7 @@ - [ ] Resuming a long claude session takes a couple of seconds for the entire buffer to load in, it looks like it's scrolling down for a couple seconds. - In raw alacritty this is instant, so there's some sort of performance issue with patterm's terminal emulation. + # On Hold - [ ] There's a unicode being displayed in opencode [ON HOLD] - Investigated 2026-05-14: patterm passes ghostty grapheme codepoints diff --git a/internal/app/app.go b/internal/app/app.go index e150b10..3d82f03 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1622,6 +1622,13 @@ func (st *uiState) closePalette(action paletteAction) { st.flashError(fmt.Sprintf("spawn %s: %v", action.preset.Name, err)) } + case "spawn-terminal": + l := st.layoutSnapshot() + st.launcher.SetSize(l.childCols(), l.childRows()) + if _, err := st.launcher.LaunchTerminal(nil, "terminal", "", "", nil); err != nil { + st.flashError(fmt.Sprintf("spawn terminal: %v", err)) + } + case "spawn-process-submit": if action.command == "" { restoreView() diff --git a/internal/app/palette.go b/internal/app/palette.go index a3c1311..88f6955 100644 --- a/internal/app/palette.go +++ b/internal/app/palette.go @@ -11,12 +11,13 @@ import ( // paletteAction is what the palette returns when the user picks an item. type paletteAction struct { // kind: "spawn-agent" | "spawn-process" | "spawn-process-form" | - // "spawn-process-submit" | "switch" | "kill" | "quit" | - // "cancel" | "pad-delete" | "pad-rename" | "pad-rename-form" | - // "pad-rename-submit" | "pad-edit" | "agent-rename" | - // "agent-rename-form" | "agent-rename-submit" | "agent-close" | - // "proc-rename" | "proc-rename-form" | "proc-rename-submit" | - // "proc-delete" | "proc-stop" | "proc-restart" + // "spawn-process-submit" | "spawn-terminal" | "switch" | + // "kill" | "quit" | "cancel" | "pad-delete" | "pad-rename" | + // "pad-rename-form" | "pad-rename-submit" | "pad-edit" | + // "agent-rename" | "agent-rename-form" | "agent-rename-submit" | + // "agent-close" | "proc-rename" | "proc-rename-form" | + // "proc-rename-submit" | "proc-delete" | "proc-stop" | + // "proc-restart" kind string // For spawn-agent / spawn-process, the preset to launch. @@ -276,6 +277,16 @@ func (p *paletteState) allItems() []paletteItem { }) } + // "New Terminal" — bare interactive $SHELL pane. Distinct from + // "Run process: …" presets in that it spawns a KindTerminal (which + // disappears from the sidebar on exit rather than sticking around + // for restart). One quick keystroke; no form. + out = append(out, paletteItem{ + label: "New Terminal", + hint: "bare interactive $SHELL · removed on exit", + action: paletteAction{kind: "spawn-terminal"}, + }) + // Freeform "Spawn process…" entry. Opens a sub-form for typing an // arbitrary command line and ticking "relaunch on exit". The action // kind is intercepted by acceptOrEnterForm so accept switches the @@ -288,14 +299,14 @@ func (p *paletteState) allItems() []paletteItem { action: paletteAction{kind: "spawn-process-form"}, }) - // Kill entries last among the action rows, before Quit. Mirror the + // Close entries last among the action rows, before Quit. Mirror the // "(current)" marker from switch entries so the focused tab is - // obvious when scanning the kill list. + // obvious when scanning the close list. for _, c := range p.children { if c.Status() != StatusRunning { continue } - label := "Kill " + c.DisplayName() + label := "Close " + c.DisplayName() if c.ID == p.focused { label = "• " + label + " (current)" } diff --git a/internal/app/session.go b/internal/app/session.go index 83d3e17..dc90ea1 100644 --- a/internal/app/session.go +++ b/internal/app/session.go @@ -433,6 +433,23 @@ func (s *Session) reapChild(c *Child, runID uint64) { if !c.restarting.Load() { c.cleanupOwnedPaths() } + // Terminals are ephemeral: unlike command entries (kept around for + // restart_process) and agents (which the user clears via close_process + // once they're done with the corpse), an exited terminal has nothing + // useful left to do. Drop it from the session so it disappears from + // the Processes sidebar / switch list immediately. + if c.Kind == KindTerminal && !c.restarting.Load() { + c.teardownPTY() + s.mu.Lock() + delete(s.children, c.ID) + for i, oid := range s.order { + if oid == c.ID { + s.order = append(s.order[:i], s.order[i+1:]...) + break + } + } + s.mu.Unlock() + } } // killDescendantsOf terminates every still-live direct child of diff --git a/internal/app/tree.go b/internal/app/tree.go index 3568ba5..b3cf035 100644 --- a/internal/app/tree.go +++ b/internal/app/tree.go @@ -96,17 +96,24 @@ func firstRunningAgentID(children []*Child) string { } // processList returns every top-level command/terminal entry in spawn -// order, regardless of running state. The Processes sidebar section -// keeps showing exited entries so the user can see what just died (and -// because Session retains KindCommand entries for restart). +// order. Exited KindCommand entries remain visible so the user can see +// what just died and reach restart_process; exited KindTerminal entries +// are filtered out because terminals are ephemeral and have no restart +// path (Session also drops them in reapChild — this filter is defensive +// for any window between exit and deletion). func processList(children []*Child) []*Child { out := make([]*Child, 0, len(children)) for _, c := range children { if c.ParentID != "" { continue } - if c.Kind == KindCommand || c.Kind == KindTerminal { + switch c.Kind { + case KindCommand: out = append(out, c) + case KindTerminal: + if c.Status() == StatusRunning { + out = append(out, c) + } } } return out diff --git a/internal/preset/preset.go b/internal/preset/preset.go index 697e54a..ec73469 100644 --- a/internal/preset/preset.go +++ b/internal/preset/preset.go @@ -300,14 +300,6 @@ func ensureDefaults(base string) error { "^\\s*>_" ] } -`, - }, - { - "presets/processes/shell.json", - `{ - "name": "shell", - "argv": ["__SHELL__"] -} `, }, } @@ -319,15 +311,7 @@ func ensureDefaults(base string) error { if err := os.MkdirAll(filepath.Dir(full), 0o700); err != nil { return err } - body := d.body - if strings.Contains(body, "__SHELL__") { - shell := os.Getenv("SHELL") - if shell == "" { - shell = "/bin/sh" - } - body = strings.ReplaceAll(body, "__SHELL__", shell) - } - if err := os.WriteFile(full, []byte(body), 0o600); err != nil { + if err := os.WriteFile(full, []byte(d.body), 0o600); err != nil { return err } }