Fix command palette over focused scratchpad
The stdin loop's scratchpad-input branch ran before the palette branch and silently dropped every byte except a handful of app-level chords, so palette typing and Esc never reached the palette while a pad was focused. Skip the pad-input branch whenever st.palette != nil. closePalette also called repaintFocused() on cancel / no-op action paths, which paints the empty focused-child slot (focusedID == "" while a pad is focused) and leaves the palette's top border drawn over the pad. Route those branches through a restoreView helper that picks repaintFocusedPad when a pad is focused. Switching from a pad to a child via the palette now clears the pad focus and wipes the viewport, matching focusProcess's pad-exit path. Adds a harness scenario (palette_over_scratchpad) that opens a pad, opens the palette, types a query, and verifies that Esc leaves the pad correctly repainted with no palette chrome lingering.
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -63,6 +63,17 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
renders the canonical `--flag` form.
|
||||
|
||||
### Fixed
|
||||
- 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
|
||||
re-opened the palette. The stdin loop's scratchpad-input branch ran
|
||||
before the palette branch and silently dropped every byte except a
|
||||
handful of app-level chords, so palette filter input and Esc never
|
||||
reached `palette.handleInput`. The palette branch now takes
|
||||
precedence whenever the palette is open, and `closePalette` repaints
|
||||
the pad (instead of the empty focused-child slot) on cancel / no-op
|
||||
action. Switching from a pad to a child via the palette now clears
|
||||
the pad focus and wipes the viewport, matching `focusProcess`.
|
||||
- Tab bar and bottom status row no longer get overwritten by long
|
||||
claude / codex sessions. Three holes were letting child output land
|
||||
on the chrome: (1) absolute cursor moves — CUP / HVP / VPA — added
|
||||
|
||||
4
TODO.md
4
TODO.md
@@ -12,10 +12,6 @@
|
||||
still feels slow after ≥15 minutes — the structural drivers the
|
||||
audit named are all addressed, so a remaining symptom is a new
|
||||
one and probably wants fresh profiling.
|
||||
- [ ] Opening the command palette with a scratchpad open creates very buggy ui.
|
||||
- Typing into the command palette doesn't work at all
|
||||
- Hitting esc causes buggy chrome, the top border of the command palette is still visible
|
||||
- This is only fixed by Ctrl + W, hitting esc again to close the palette, then re-opening it when over an agent view.
|
||||
- [ ] Context aware command palette options
|
||||
- Options for current scratchpad (delete, rename, edit) at the top when a scratchpad is selected.
|
||||
- Options for current agent (rename [renames tab], close) at the top when an agent is selected.
|
||||
|
||||
@@ -1178,7 +1178,11 @@ func (st *uiState) processStdin(chunk []byte) {
|
||||
// palette, Ctrl-WASD focus, Ctrl-B scrollback) fall through to
|
||||
// the handlers below; everything else is swallowed silently so
|
||||
// typing into a pad view can't leak to a child PTY.
|
||||
if st.focusedPad != "" {
|
||||
//
|
||||
// When the palette is open we skip this block entirely so the
|
||||
// palette handler below receives every byte. Otherwise typing
|
||||
// (and Esc) get swallowed here and the palette appears wedged.
|
||||
if st.focusedPad != "" && st.palette == nil {
|
||||
if b == 0x1b { // ESC or CSI
|
||||
if n := csiLen(chunk, i); n > 0 {
|
||||
final := chunk[i+n-1]
|
||||
@@ -1553,16 +1557,32 @@ func (st *uiState) closePalette(action paletteAction) {
|
||||
st.outMu.Unlock()
|
||||
st.clearScreen()
|
||||
|
||||
// When a scratchpad is focused, the "main viewport" is showing the
|
||||
// pad's rendered body, not a child PTY. repaintFocused() would draw
|
||||
// an empty state (focusedID == "") and leave the previous palette's
|
||||
// top border visible in the pad area. Use the pad-aware helper for
|
||||
// any branch below that wants to restore the prior view.
|
||||
restoreView := func() {
|
||||
st.mu.Lock()
|
||||
padFocused := st.focusedPad != ""
|
||||
st.mu.Unlock()
|
||||
if padFocused {
|
||||
st.repaintFocusedPad()
|
||||
return
|
||||
}
|
||||
st.repaintFocused()
|
||||
}
|
||||
|
||||
switch action.kind {
|
||||
case "", "cancel":
|
||||
st.repaintFocused()
|
||||
restoreView()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
st.drawStatusLine()
|
||||
|
||||
case "spawn-agent":
|
||||
if action.preset == nil {
|
||||
st.repaintFocused()
|
||||
restoreView()
|
||||
return
|
||||
}
|
||||
l := st.layoutSnapshot()
|
||||
@@ -1575,7 +1595,7 @@ func (st *uiState) closePalette(action paletteAction) {
|
||||
|
||||
case "spawn-process":
|
||||
if action.preset == nil {
|
||||
st.repaintFocused()
|
||||
restoreView()
|
||||
return
|
||||
}
|
||||
l := st.layoutSnapshot()
|
||||
@@ -1586,7 +1606,7 @@ func (st *uiState) closePalette(action paletteAction) {
|
||||
|
||||
case "spawn-process-submit":
|
||||
if action.command == "" {
|
||||
st.repaintFocused()
|
||||
restoreView()
|
||||
return
|
||||
}
|
||||
l := st.layoutSnapshot()
|
||||
@@ -1617,16 +1637,24 @@ func (st *uiState) closePalette(action paletteAction) {
|
||||
case "switch":
|
||||
c := st.sess.FindChild(action.childID)
|
||||
if c == nil || (c.Kind == KindAgent && c.Status() != StatusRunning) {
|
||||
st.repaintFocused()
|
||||
restoreView()
|
||||
return
|
||||
}
|
||||
layout := st.layoutSnapshot()
|
||||
st.mu.Lock()
|
||||
leavingPad := st.focusedPad != ""
|
||||
st.focusedPad = ""
|
||||
st.focusedID = action.childID
|
||||
st.focusedName = c.DisplayName()
|
||||
st.updateActiveAgentLocked(c)
|
||||
st.renderer = newViewportRenderer(layout)
|
||||
st.mu.Unlock()
|
||||
// Switching from a pad to a child: wipe the pad body so the
|
||||
// child's snapshot paints onto a clean canvas, mirroring
|
||||
// focusProcess.
|
||||
if leavingPad {
|
||||
st.clearViewportArea()
|
||||
}
|
||||
st.repaintFocused()
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
|
||||
28
internal/harness/scenarios/palette_over_scratchpad.json
Normal file
28
internal/harness/scenarios/palette_over_scratchpad.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "palette_over_scratchpad",
|
||||
"cols": 120,
|
||||
"rows": 30,
|
||||
"steps": [
|
||||
{
|
||||
"type": "mcp_call",
|
||||
"method": "scratchpad_write",
|
||||
"params": { "name": "pad-marker.md", "content": "# Pad Heading\n\nzealot-marker body line" }
|
||||
},
|
||||
{ "type": "wait_stable", "timeout_ms": 2000 },
|
||||
{ "type": "send_chord", "chord": "ctrl-s" },
|
||||
{ "type": "wait_text", "contains": "zealot-marker", "timeout_ms": 5000 },
|
||||
{ "type": "assert_contains", "contains": "Pad Heading" },
|
||||
|
||||
{ "type": "send_chord", "chord": "ctrl-k" },
|
||||
{ "type": "wait_stable", "timeout_ms": 2000 },
|
||||
{ "type": "send_text", "text": "quit" },
|
||||
{ "type": "wait_text", "contains": "quit", "timeout_ms": 5000 },
|
||||
{ "type": "assert_contains", "contains": "quit" },
|
||||
|
||||
{ "type": "send_chord", "chord": "escape" },
|
||||
{ "type": "wait_text", "contains": "zealot-marker", "timeout_ms": 5000 },
|
||||
{ "type": "assert_contains", "contains": "Pad Heading" },
|
||||
{ "type": "assert_contains", "contains": "zealot-marker" },
|
||||
{ "type": "assert_not_contains", "contains": "quit" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user