diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d556f..97c941e 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. - User-created top-level command processes now survive a patterm restart. Each spawn (palette form, command preset, or MCP `spawn_process` with `kind=command`) writes a record to @@ -64,6 +71,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. - CLI flag parsing switched from Go's stdlib `flag` to `spf13/pflag`. `--project` (and the internal `--socket` / `--identity` / `--scenario` / `--patterm-bin` flags) are now the only accepted form @@ -71,6 +83,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. - Opening the command palette while a scratchpad was focused left the palette wedged — typing did nothing and Esc left the palette's top border drawn over the pad until you closed the pad with Ctrl-W and diff --git a/TODO.md b/TODO.md index d6a8774..62018d3 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +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. - - # 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 8bf515d..9000478 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1607,6 +1607,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 11f72c5..d0859c4 100644 --- a/internal/app/session.go +++ b/internal/app/session.go @@ -403,6 +403,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 39039c1..1c5c6f6 100644 --- a/internal/preset/preset.go +++ b/internal/preset/preset.go @@ -250,14 +250,6 @@ func ensureDefaults(base string) error { "^\\s*>_" ] } -`, - }, - { - "presets/processes/shell.json", - `{ - "name": "shell", - "argv": ["__SHELL__"] -} `, }, } @@ -269,15 +261,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 } }