Add context-aware items to the command palette
When opened with Ctrl-K, the palette now prepends entries for whatever is currently focused: - Focused scratchpad: Delete / Rename (inline form) / Edit (fire-and- forget zed launch with stdio detached so the TUI is not suspended). - Focused agent: Rename (inline form) / Close. - Focused process: Rename / Delete (drops the entry; SIGKILL if alive) / Stop (SIGTERM, keep entry) / Restart (same argv). The rename UX is a single-field inline form that mirrors the existing spawn-process form, so the modal-input contract is unchanged. scratchpad.Store grows Delete / Rename / Path so the palette can act on a pad file by name. focusedPad is plumbed onto uiState ahead of the scratchpad-focus UI work; until that lands it stays empty and the scratchpad-context entries simply never surface. Tested with palette_context_test.go and a new rename_process_via_palette harness scenario.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -305,7 +306,8 @@ type uiState struct {
|
||||
// focusedPad names the scratchpad currently rendered in the main
|
||||
// viewport. When non-empty, focusedID is "" and the host renders
|
||||
// pad content instead of forwarding child PTY output. Mutually
|
||||
// exclusive with focusedID.
|
||||
// exclusive with focusedID. The palette also reads this to surface
|
||||
// scratchpad-specific actions at the top of the command list.
|
||||
focusedPad string
|
||||
// padOffset is the index of the top-most rendered row in the
|
||||
// markdown-formatted view of focusedPad. Reset when focus moves to
|
||||
@@ -317,6 +319,7 @@ type uiState struct {
|
||||
// switch resets the offset cleanly.
|
||||
padOffsetName string
|
||||
|
||||
|
||||
// activeAgentID tracks which top-level agent tab "owns" the agent
|
||||
// tree section of the sidebar. It only updates when focus lands on
|
||||
// an agent (or one of its sub-agents), so the agent tree stays
|
||||
@@ -1529,7 +1532,7 @@ func (st *uiState) scrollFocusedViewportToBottom() {
|
||||
}
|
||||
|
||||
func (st *uiState) openPaletteLocked() {
|
||||
st.palette = newPalette(st.sess.Children(), st.focusedID, st.presets)
|
||||
st.palette = newPalette(st.sess.Children(), st.focusedID, st.focusedPad, st.presets)
|
||||
// Push a "no kitty flags" entry onto the host terminal's keyboard
|
||||
// stack so palette input arrives in plain legacy form regardless of
|
||||
// what the focused child pushed. Codex/ratatui enables kitty mode
|
||||
@@ -1674,9 +1677,207 @@ func (st *uiState) closePalette(action paletteAction) {
|
||||
|
||||
case "quit":
|
||||
st.requestExit()
|
||||
|
||||
case "pad-delete":
|
||||
st.handlePadDelete(action.padName)
|
||||
|
||||
case "pad-rename-submit":
|
||||
st.handlePadRename(action.padName, action.newName)
|
||||
|
||||
case "pad-edit":
|
||||
st.handlePadEdit(action.padName)
|
||||
|
||||
case "agent-rename-submit", "proc-rename-submit":
|
||||
st.handleChildRename(action.childID, action.newName)
|
||||
|
||||
case "agent-close", "proc-delete":
|
||||
st.handleChildClose(action.childID, action.kind == "proc-delete")
|
||||
|
||||
case "proc-stop":
|
||||
st.handleProcStop(action.childID)
|
||||
|
||||
case "proc-restart":
|
||||
st.handleProcRestart(action.childID)
|
||||
}
|
||||
}
|
||||
|
||||
func (st *uiState) handlePadDelete(name string) {
|
||||
if name == "" || st.pads == nil {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
if err := st.pads.Delete(name); err != nil {
|
||||
st.flashError(fmt.Sprintf("delete %s: %v", name, err))
|
||||
return
|
||||
}
|
||||
st.mu.Lock()
|
||||
if st.focusedPad == name {
|
||||
st.focusedPad = ""
|
||||
}
|
||||
st.mu.Unlock()
|
||||
st.scratchpadsChanged()
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
func (st *uiState) handlePadRename(oldName, newName string) {
|
||||
if oldName == "" || newName == "" || st.pads == nil {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
if oldName == newName {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
if err := st.pads.Rename(oldName, newName); err != nil {
|
||||
st.flashError(fmt.Sprintf("rename %s: %v", oldName, err))
|
||||
return
|
||||
}
|
||||
st.mu.Lock()
|
||||
if st.focusedPad == oldName {
|
||||
st.focusedPad = newName
|
||||
}
|
||||
st.mu.Unlock()
|
||||
st.scratchpadsChanged()
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
// handlePadEdit launches an external editor (zed) on the focused
|
||||
// scratchpad file. Fire-and-forget: we Start() the editor with
|
||||
// stdin/stdout/stderr redirected to /dev/null and call Process.Release()
|
||||
// so the patterm process doesn't accumulate zombies. The editor opens
|
||||
// in its own window without suspending the TUI.
|
||||
func (st *uiState) handlePadEdit(name string) {
|
||||
if name == "" || st.pads == nil {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
path, err := st.pads.Path(name)
|
||||
if err != nil {
|
||||
st.flashError(fmt.Sprintf("edit %s: %v", name, err))
|
||||
return
|
||||
}
|
||||
null, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
st.flashError(fmt.Sprintf("edit %s: open /dev/null: %v", name, err))
|
||||
return
|
||||
}
|
||||
cmd := exec.Command("zed", path)
|
||||
cmd.Stdin = null
|
||||
cmd.Stdout = null
|
||||
cmd.Stderr = null
|
||||
if err := cmd.Start(); err != nil {
|
||||
_ = null.Close()
|
||||
st.flashError(fmt.Sprintf("edit %s: %v", name, err))
|
||||
return
|
||||
}
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Release()
|
||||
}
|
||||
_ = null.Close()
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
func (st *uiState) handleChildRename(childID, newName string) {
|
||||
if childID == "" || newName == "" {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
c := st.sess.FindChild(childID)
|
||||
if c == nil {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
c.SetName(newName)
|
||||
st.mu.Lock()
|
||||
if st.focusedID == childID {
|
||||
st.focusedName = newName
|
||||
}
|
||||
st.mu.Unlock()
|
||||
st.chromeCacheMu.Lock()
|
||||
st.tabBarCache = ""
|
||||
st.sidebarCache = ""
|
||||
st.chromeCacheMu.Unlock()
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
// handleChildClose removes a child entry entirely. For agents this is
|
||||
// equivalent to a SIGTERM kill (the entry is ephemeral and disappears
|
||||
// from the session once the PTY exits). For command processes it's
|
||||
// equivalent to the MCP close_process tool: SIGKILL if alive, then
|
||||
// drop the entry so it stops appearing in the switch/restart lists.
|
||||
func (st *uiState) handleChildClose(childID string, kill bool) {
|
||||
if childID == "" {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
c := st.sess.FindChild(childID)
|
||||
if c == nil {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
c.SetAutoRestart(false)
|
||||
if kill {
|
||||
_ = st.sess.Close(childID, syscall.SIGKILL)
|
||||
} else {
|
||||
_ = st.sess.Kill(childID, syscall.SIGTERM)
|
||||
}
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
func (st *uiState) handleProcStop(childID string) {
|
||||
if childID == "" {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
c := st.sess.FindChild(childID)
|
||||
if c == nil {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
c.SetAutoRestart(false)
|
||||
_ = st.sess.Kill(childID, syscall.SIGTERM)
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
func (st *uiState) handleProcRestart(childID string) {
|
||||
if childID == "" {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
c := st.sess.FindChild(childID)
|
||||
if c == nil {
|
||||
st.repaintFocused()
|
||||
return
|
||||
}
|
||||
layout := st.layoutSnapshot()
|
||||
if err := st.sess.Restart(childID, syscall.SIGTERM, layout.childCols(), layout.childRows()); err != nil {
|
||||
st.flashError(fmt.Sprintf("restart %s: %v", c.DisplayName(), err))
|
||||
return
|
||||
}
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
// flashError surfaces a spawn/etc. failure in the status line until the
|
||||
// next attention update overwrites it. stderr is hidden under the alt
|
||||
// screen so we can't rely on Fprintln(os.Stderr).
|
||||
|
||||
Reference in New Issue
Block a user