Bundles the in-flight work into the first tagged release. See CHANGELOG.md `[0.0.1] - 2026-05-14` for the full per-change list. Highlights: - Sidebar / chrome stability: clamp absolute cursor positioning and printable bytes to the viewport so long-running TUIs (claude, codex) can't spray into the right rail; bound tab bar's row clear to the viewport width so the rail isn't wiped on every tab redraw; flag scroll escapes (RI/IND/NEL/SU/SD/IL/DL) and clamp `CSI 0/1/2 J`/`K` to viewport columns. - Palette: "Spawn process…" form, macros (`sw `, `k `, `sp `), kill entries mark the focused tab, dead agents drop out of the switch list. - Sidebar: split into Processes (session-wide) + Agent Tree (per-active-agent) sections; relaunch indicator; Ctrl+W/S walks the combined list, Ctrl+A/D steps tabs. - MCP: protocol handshake (`initialize`, `tools/list`, `tools/call`, `ping`), `mcp_injection.kind = cli_override / config_env` so codex and opencode pick up the server with no file writes, `lifecycle` help topic and tool-description cleanup-duty pointers. - Lifecycle: orchestrator-spawned children cascade-killed when the parent dies; orchestrator-injected prompts end with CR + delayed Enter so claude submits cleanly.
This commit is contained in:
@@ -27,6 +27,12 @@ type viewportRenderer struct {
|
||||
// OnPTYOut consumes the flag and invalidates the sidebar chrome
|
||||
// cache so the next drawSidebar repaints over the clobber.
|
||||
scrolled bool
|
||||
|
||||
// skipUTF8 is set when the current multi-byte UTF-8 character started
|
||||
// past the viewport's right edge. The starter byte was dropped, so
|
||||
// the remaining continuation bytes must be dropped too instead of
|
||||
// leaking into the sidebar columns.
|
||||
skipUTF8 bool
|
||||
}
|
||||
|
||||
type viewportState int
|
||||
@@ -45,7 +51,7 @@ const (
|
||||
|
||||
func newViewportRenderer(l terminalLayout) *viewportRenderer {
|
||||
return &viewportRenderer{
|
||||
shifter: newCursorShifter(int(l.mainTop)-1, int(l.childRows())),
|
||||
shifter: newCursorShifter(int(l.mainTop)-1, int(l.childRows()), int(l.childCols())),
|
||||
layout: l,
|
||||
row: 1,
|
||||
col: 1,
|
||||
@@ -56,7 +62,7 @@ func (vr *viewportRenderer) SetLayout(l terminalLayout) {
|
||||
vr.mu.Lock()
|
||||
defer vr.mu.Unlock()
|
||||
vr.layout = l
|
||||
vr.shifter.SetGeometry(int(l.mainTop)-1, int(l.childRows()))
|
||||
vr.shifter.SetGeometry(int(l.mainTop)-1, int(l.childRows()), int(l.childCols()))
|
||||
}
|
||||
|
||||
func (vr *viewportRenderer) Render(in []byte) []byte {
|
||||
@@ -98,8 +104,7 @@ func (vr *viewportRenderer) feed(b byte) {
|
||||
vr.buf = append(vr.buf, b)
|
||||
return
|
||||
}
|
||||
vr.pending.WriteByte(b)
|
||||
vr.advancePrintable(b)
|
||||
vr.feedPrintable(b)
|
||||
case vpEsc:
|
||||
vr.buf = append(vr.buf, b)
|
||||
switch b {
|
||||
@@ -286,6 +291,9 @@ func (vr *viewportRenderer) clearViewportToCursor() string {
|
||||
if col < 1 {
|
||||
col = 1
|
||||
}
|
||||
if col > cols {
|
||||
col = cols
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("\x1b7")
|
||||
for r := 1; r < row; r++ {
|
||||
@@ -318,6 +326,60 @@ func (vr *viewportRenderer) clearLine(n int) string {
|
||||
}
|
||||
}
|
||||
|
||||
// feedPrintable handles one non-ESC byte in the vpNormal state. It both
|
||||
// advances vr's cursor model and decides whether the byte should be
|
||||
// forwarded to the host. Bytes that would land past the viewport's
|
||||
// right edge (childCols) are dropped so a child whose internal column
|
||||
// state drifted past the viewport can't spray into the sidebar columns.
|
||||
// UTF-8 continuation bytes follow the fate of their starter so a
|
||||
// multi-byte glyph drops as a unit.
|
||||
func (vr *viewportRenderer) feedPrintable(b byte) {
|
||||
// Control codes (CR, LF, BS, TAB, BEL, etc.) move the cursor or
|
||||
// signal state and must always be forwarded. They never produce
|
||||
// glyphs, so they can't clobber the sidebar themselves.
|
||||
if b < 0x20 || b == 0x7f {
|
||||
vr.pending.WriteByte(b)
|
||||
switch b {
|
||||
case '\r':
|
||||
vr.col = 1
|
||||
case '\n':
|
||||
vr.row++
|
||||
case '\b':
|
||||
if vr.col > 1 {
|
||||
vr.col--
|
||||
}
|
||||
case '\t':
|
||||
vr.col += 8 - ((vr.col - 1) % 8)
|
||||
}
|
||||
vr.skipUTF8 = false
|
||||
vr.clampCursor()
|
||||
return
|
||||
}
|
||||
// UTF-8 continuation byte (10xxxxxx) belongs to the current glyph.
|
||||
if b >= 0x80 && b < 0xC0 {
|
||||
if vr.skipUTF8 {
|
||||
return
|
||||
}
|
||||
vr.pending.WriteByte(b)
|
||||
return
|
||||
}
|
||||
// Glyph starter (ASCII 0x20..0x7E or UTF-8 leading byte 0xC0+). If
|
||||
// the cursor sits past the viewport we'd be spraying into the
|
||||
// sidebar columns — drop the glyph (and the continuation bytes that
|
||||
// follow, via skipUTF8).
|
||||
maxCol := int(vr.layout.childCols())
|
||||
if maxCol > 0 && vr.col > maxCol {
|
||||
vr.skipUTF8 = b >= 0xC0
|
||||
return
|
||||
}
|
||||
vr.skipUTF8 = false
|
||||
vr.pending.WriteByte(b)
|
||||
vr.col++
|
||||
vr.clampCursor()
|
||||
}
|
||||
|
||||
// advancePrintable is retained for tests that exercise cursor tracking
|
||||
// directly; the runtime path goes through feedPrintable.
|
||||
func (vr *viewportRenderer) advancePrintable(b byte) {
|
||||
switch b {
|
||||
case '\r':
|
||||
@@ -331,7 +393,7 @@ func (vr *viewportRenderer) advancePrintable(b byte) {
|
||||
case '\t':
|
||||
vr.col += 8 - ((vr.col - 1) % 8)
|
||||
default:
|
||||
if b >= 0x20 && b != 0x7f {
|
||||
if b >= 0x20 && b != 0x7f && (b < 0x80 || b >= 0xC0) {
|
||||
vr.col++
|
||||
}
|
||||
}
|
||||
@@ -389,7 +451,15 @@ func (vr *viewportRenderer) clampCursor() {
|
||||
if max := int(vr.layout.childRows()); vr.row > max {
|
||||
vr.row = max
|
||||
}
|
||||
if max := int(vr.layout.childCols()); vr.col > max {
|
||||
vr.col = max
|
||||
// Intentionally do NOT clamp vr.col to childCols here. feedPrintable
|
||||
// drops glyphs once vr.col exceeds childCols (so a child whose
|
||||
// internal column state drifted past the viewport can't spray bytes
|
||||
// into the sidebar). If we clamped col back to childCols on every
|
||||
// printable, every subsequent byte would look like it was still "at
|
||||
// the right margin" and would write again. We cap at childCols+1
|
||||
// instead so clear-line bookkeeping doesn't see arbitrarily large
|
||||
// numbers.
|
||||
if max := int(vr.layout.childCols()); vr.col > max+1 {
|
||||
vr.col = max + 1
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user