This commit is contained in:
2026-05-15 00:28:06 +01:00
parent 2f969fa215
commit 0d578d54f1
31 changed files with 3209 additions and 164 deletions

View File

@@ -67,6 +67,29 @@ func (cs *cursorShifter) clampCol(col int) int {
return col
}
// clampHostRow returns a host-coordinate row clamped to the viewport
// rows mainTop..mainBottom. A child whose internal row state drifted
// past the viewport (long-running claude / codex sessions) can issue a
// CUP / HVP / VPA aimed at row hostRows; after the +rowOffset shift the
// raw host target sits past the viewport bottom (the status row) or
// above the viewport top (the tab bar). Without clamping the host
// cursor lands on the chrome and the next printable wipes it. childRows
// == 0 (uninitialised shifter, only seen in tests) disables clamping.
func (cs *cursorShifter) clampHostRow(r int) int {
if cs.childRows <= 0 {
return r
}
minR := cs.rowOffset + 1
maxR := cs.rowOffset + cs.childRows
if r < minR {
return minR
}
if r > maxR {
return maxR
}
return r
}
// Shift consumes a chunk of PTY-master bytes, applies row offsets to
// any complete CUP/HVP/VPA/DECSTBM sequences, and returns the rewritten
// bytes. Partial sequences are buffered across calls so a CSI that
@@ -206,7 +229,7 @@ func (cs *cursorShifter) emitCSI() {
cs.pending.Write(cs.buf)
return
}
r += cs.rowOffset
r = cs.clampHostRow(r + cs.rowOffset)
c = cs.clampCol(c)
cs.pending.WriteString("\x1b[")
cs.pending.WriteString(strconv.Itoa(r))
@@ -226,13 +249,14 @@ func (cs *cursorShifter) emitCSI() {
cs.pending.WriteString(strconv.Itoa(c))
cs.pending.WriteByte(final)
case 'd':
// VPA: row.
// VPA: row. Clamp to the viewport so a child that drifted
// past its row count can't land the host cursor on the status row.
r, ok := parseOneParam(paramsRaw, 1)
if !ok {
cs.pending.Write(cs.buf)
return
}
r += cs.rowOffset
r = cs.clampHostRow(r + cs.rowOffset)
cs.pending.WriteString("\x1b[")
cs.pending.WriteString(strconv.Itoa(r))
cs.pending.WriteByte(final)