Rename Kill to Close, add New Terminal palette entry, clean up exited terminals
- Palette's per-child "Kill <name>" action is now labelled "Close <name>" (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.
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -7,6 +7,13 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- User-created top-level command processes now survive a patterm
|
||||||
restart. Each spawn (palette form, command preset, or MCP
|
restart. Each spawn (palette form, command preset, or MCP
|
||||||
`spawn_process` with `kind=command`) writes a record to
|
`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.
|
after a child program disables mouse tracking.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- The palette's per-child "Kill <name>" action is now labelled
|
||||||
|
"Close <name>". 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`.
|
- CLI flag parsing switched from Go's stdlib `flag` to `spf13/pflag`.
|
||||||
`--project` (and the internal `--socket` / `--identity` /
|
`--project` (and the internal `--socket` / `--identity` /
|
||||||
`--scenario` / `--patterm-bin` flags) are now the only accepted form
|
`--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.
|
renders the canonical `--flag` form.
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- Opening the command palette while a scratchpad was focused left the
|
||||||
palette wedged — typing did nothing and Esc left the palette's top
|
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
|
border drawn over the pad until you closed the pad with Ctrl-W and
|
||||||
|
|||||||
5
TODO.md
5
TODO.md
@@ -1,8 +1,3 @@
|
|||||||
- [ ] We should probably rename the Kill <Process> terminology to Close <Process> 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
|
# On Hold
|
||||||
- [ ] There's a unicode <?> being displayed in opencode [ON HOLD]
|
- [ ] There's a unicode <?> being displayed in opencode [ON HOLD]
|
||||||
- Investigated 2026-05-14: patterm passes ghostty grapheme codepoints
|
- Investigated 2026-05-14: patterm passes ghostty grapheme codepoints
|
||||||
|
|||||||
@@ -1607,6 +1607,13 @@ func (st *uiState) closePalette(action paletteAction) {
|
|||||||
st.flashError(fmt.Sprintf("spawn %s: %v", action.preset.Name, err))
|
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":
|
case "spawn-process-submit":
|
||||||
if action.command == "" {
|
if action.command == "" {
|
||||||
restoreView()
|
restoreView()
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import (
|
|||||||
// paletteAction is what the palette returns when the user picks an item.
|
// paletteAction is what the palette returns when the user picks an item.
|
||||||
type paletteAction struct {
|
type paletteAction struct {
|
||||||
// kind: "spawn-agent" | "spawn-process" | "spawn-process-form" |
|
// kind: "spawn-agent" | "spawn-process" | "spawn-process-form" |
|
||||||
// "spawn-process-submit" | "switch" | "kill" | "quit" |
|
// "spawn-process-submit" | "spawn-terminal" | "switch" |
|
||||||
// "cancel" | "pad-delete" | "pad-rename" | "pad-rename-form" |
|
// "kill" | "quit" | "cancel" | "pad-delete" | "pad-rename" |
|
||||||
// "pad-rename-submit" | "pad-edit" | "agent-rename" |
|
// "pad-rename-form" | "pad-rename-submit" | "pad-edit" |
|
||||||
// "agent-rename-form" | "agent-rename-submit" | "agent-close" |
|
// "agent-rename" | "agent-rename-form" | "agent-rename-submit" |
|
||||||
// "proc-rename" | "proc-rename-form" | "proc-rename-submit" |
|
// "agent-close" | "proc-rename" | "proc-rename-form" |
|
||||||
// "proc-delete" | "proc-stop" | "proc-restart"
|
// "proc-rename-submit" | "proc-delete" | "proc-stop" |
|
||||||
|
// "proc-restart"
|
||||||
kind string
|
kind string
|
||||||
|
|
||||||
// For spawn-agent / spawn-process, the preset to launch.
|
// 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
|
// Freeform "Spawn process…" entry. Opens a sub-form for typing an
|
||||||
// arbitrary command line and ticking "relaunch on exit". The action
|
// arbitrary command line and ticking "relaunch on exit". The action
|
||||||
// kind is intercepted by acceptOrEnterForm so accept switches the
|
// kind is intercepted by acceptOrEnterForm so accept switches the
|
||||||
@@ -288,14 +299,14 @@ func (p *paletteState) allItems() []paletteItem {
|
|||||||
action: paletteAction{kind: "spawn-process-form"},
|
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
|
// "(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 {
|
for _, c := range p.children {
|
||||||
if c.Status() != StatusRunning {
|
if c.Status() != StatusRunning {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
label := "Kill " + c.DisplayName()
|
label := "Close " + c.DisplayName()
|
||||||
if c.ID == p.focused {
|
if c.ID == p.focused {
|
||||||
label = "• " + label + " (current)"
|
label = "• " + label + " (current)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,6 +403,23 @@ func (s *Session) reapChild(c *Child, runID uint64) {
|
|||||||
if !c.restarting.Load() {
|
if !c.restarting.Load() {
|
||||||
c.cleanupOwnedPaths()
|
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
|
// killDescendantsOf terminates every still-live direct child of
|
||||||
|
|||||||
@@ -96,17 +96,24 @@ func firstRunningAgentID(children []*Child) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processList returns every top-level command/terminal entry in spawn
|
// processList returns every top-level command/terminal entry in spawn
|
||||||
// order, regardless of running state. The Processes sidebar section
|
// order. Exited KindCommand entries remain visible so the user can see
|
||||||
// keeps showing exited entries so the user can see what just died (and
|
// what just died and reach restart_process; exited KindTerminal entries
|
||||||
// because Session retains KindCommand entries for restart).
|
// 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 {
|
func processList(children []*Child) []*Child {
|
||||||
out := make([]*Child, 0, len(children))
|
out := make([]*Child, 0, len(children))
|
||||||
for _, c := range children {
|
for _, c := range children {
|
||||||
if c.ParentID != "" {
|
if c.ParentID != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if c.Kind == KindCommand || c.Kind == KindTerminal {
|
switch c.Kind {
|
||||||
|
case KindCommand:
|
||||||
out = append(out, c)
|
out = append(out, c)
|
||||||
|
case KindTerminal:
|
||||||
|
if c.Status() == StatusRunning {
|
||||||
|
out = append(out, c)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -250,14 +250,6 @@ func ensureDefaults(base string) error {
|
|||||||
"^\\s*>_"
|
"^\\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 {
|
if err := os.MkdirAll(filepath.Dir(full), 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
body := d.body
|
if err := os.WriteFile(full, []byte(d.body), 0o600); err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user