Fix sidebar repaint and command restart navigation

This commit is contained in:
2026-05-14 22:41:24 +01:00
parent 83eb4f6b2d
commit 2f969fa215
9 changed files with 247 additions and 29 deletions

View File

@@ -298,6 +298,35 @@ func (st *uiState) focusProcess(processID string) {
st.drawStatusLine()
}
func (st *uiState) restartFocusedCommand(processID string) {
c := st.sess.FindChild(processID)
if c == nil || c.Kind != KindCommand {
return
}
layout := st.layoutSnapshot()
renderer := newViewportRenderer(layout)
st.mu.Lock()
st.focusedID = c.ID
st.focusedName = c.DisplayName()
st.renderer = renderer
st.repaintNextPTY = c.ID
st.repaintNextPTYBudget = 8
st.mu.Unlock()
st.outMu.Lock()
_, _ = os.Stdout.Write(renderer.ClearViewport())
st.outMu.Unlock()
if err := st.sess.Restart(c.ID, syscall.SIGTERM, layout.childCols(), layout.childRows()); err != nil {
st.flashError(fmt.Sprintf("restart %s: %v", c.DisplayName(), err))
return
}
st.moveToViewportOrigin()
st.drawTabBar()
st.drawSidebar()
st.drawStatusLine()
}
// updateActiveAgentLocked records the active agent root for the agent
// tree section whenever focus lands on an agent or one of its
// sub-agents. Focusing a top-level command process leaves the previous
@@ -513,14 +542,13 @@ func (st *uiState) OnPTYOut(childID string, chunk []byte) {
_, _ = os.Stdout.Write(out)
_, _ = os.Stdout.Write([]byte("\x1b[?7h"))
st.outMu.Unlock()
// RI / IND / NEL / SU / SD / IL / DL scroll content within the host's
// scroll region, which spans every column — so any of them drags the
// right-hand sidebar's session-tree entries downward along with the
// main pane. (Codex emits an 8× RI burst on startup, which produced
// the original report.) The viewport renderer flags any chunk that
// contained one of those escapes; when set, drop the sidebar cache
// so the next drawSidebar repaints over the clobber instead of
// hitting the cache and leaving the gap visible.
// RI / IND / NEL / SU / SD / IL / DL and bottom-margin LF / VT / FF
// scroll content within the host's scroll region, which spans every
// column — so any of them drags the right-hand sidebar's session-tree
// entries along with the main pane. The viewport renderer flags any
// chunk that scrolls; when set, drop the sidebar cache so the next
// drawSidebar repaints over the clobber instead of hitting the cache
// and leaving the gap visible.
scrolled := renderer.TookScrollAction()
if scrolled {
st.chromeCacheMu.Lock()
@@ -639,13 +667,17 @@ func (st *uiState) drawStatusLine() {
if trustMsg != "" {
left = "[trust] " + trustMsg
}
// Hints decay shortest-first when the host is narrow so the focused
// Hints decay left-to-right when the host is narrow so the focused
// child name + ownership note on the left side never get clipped.
// Context-specific hints are appended so they survive longest.
hints := []string{
"Ctrl-A/D · tabs",
"Ctrl-W/S · tree",
"Ctrl-K · palette",
}
if c := st.sess.FindChild(focusID); c != nil && c.Kind == KindCommand {
hints = append(hints, "Ctrl-R · restart")
}
right := strings.Join(hints, " · ")
for len(hints) > 1 && int(cols)-len(left)-len(right) < 1 {
hints = hints[1:]
@@ -833,6 +865,7 @@ func (st *uiState) processStdin(chunk []byte) {
var pendingAction *paletteAction
var pendingNavID string
var pendingRestartID string
// Tracks the last arrow direction and the byte offset immediately
// after its CSI sequence. Some terminals emit a duplicate adjacent
@@ -928,6 +961,14 @@ func (st *uiState) processStdin(chunk []byte) {
i += adv
break
}
if hit, adv := matchCtrlChar(chunk, i, 'r'); hit {
if c := st.sess.FindChild(st.focusedID); c != nil && c.Kind == KindCommand {
flushForward()
pendingRestartID = c.ID
i += adv
break
}
}
forward = append(forward, b)
i++
@@ -941,6 +982,9 @@ func (st *uiState) processStdin(chunk []byte) {
if pendingNavID != "" {
st.focusProcess(pendingNavID)
}
if pendingRestartID != "" {
st.restartFocusedCommand(pendingRestartID)
}
}
func (st *uiState) openPaletteLocked() {
@@ -1035,7 +1079,7 @@ func (st *uiState) closePalette(action paletteAction) {
case "switch":
c := st.sess.FindChild(action.childID)
if c == nil || c.Status() != StatusRunning {
if c == nil || (c.Kind == KindAgent && c.Status() != StatusRunning) {
st.repaintFocused()
return
}