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:
@@ -5,6 +5,14 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func bytesRepeat(b byte, n int) []byte {
|
||||
out := make([]byte, n)
|
||||
for i := range out {
|
||||
out[i] = b
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestViewportRendererShiftsCursor(t *testing.T) {
|
||||
vr := newViewportRenderer(newTerminalLayout(120, 40))
|
||||
got := string(vr.Render([]byte("\x1b[H")))
|
||||
@@ -103,6 +111,65 @@ func TestViewportRendererTracksPrintableCursor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewportRendererClampsCUPColumn(t *testing.T) {
|
||||
// Layout: hostCols=120, sidebar present → childCols=91. A child that
|
||||
// thinks its viewport is the full host width could emit a CUP to col
|
||||
// 95 (inside the sidebar). The renderer must clamp the emitted CUP
|
||||
// column so the host cursor never lands in the sidebar.
|
||||
vr := newViewportRenderer(newTerminalLayout(120, 40))
|
||||
got := string(vr.Render([]byte("\x1b[5;95H")))
|
||||
if !strings.Contains(got, "\x1b[7;91H") {
|
||||
t.Fatalf("CUP col 95 should clamp to 91 (childCols): got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewportRendererClampsCHAColumn(t *testing.T) {
|
||||
vr := newViewportRenderer(newTerminalLayout(120, 40))
|
||||
got := string(vr.Render([]byte("\x1b[110G")))
|
||||
if !strings.Contains(got, "\x1b[91G") {
|
||||
t.Fatalf("CHA col 110 should clamp to 91 (childCols): got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewportRendererDropsPrintablesPastViewport(t *testing.T) {
|
||||
// A child whose internal column state drifted past the viewport
|
||||
// (childCols=91 here) might CUP to col 95 and stream text. The CUP
|
||||
// column is clamped to the viewport edge, but tracking still
|
||||
// considers the cursor "past" — so subsequent printables must drop
|
||||
// rather than walk into the sidebar columns.
|
||||
vr := newViewportRenderer(newTerminalLayout(120, 40))
|
||||
got := string(vr.Render([]byte("\x1b[5;95HCLOBBER")))
|
||||
if strings.Contains(got, "CLOBBER") || strings.Contains(got, "LOBBER") {
|
||||
t.Fatalf("printables past childCols should be dropped: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewportRendererKeepsPrintablesUpToViewportEdge(t *testing.T) {
|
||||
// Writing exactly childCols glyphs from col 1 must reach the right
|
||||
// edge unchanged — the drop kicks in only after the cursor passes
|
||||
// the last viewport column.
|
||||
vr := newViewportRenderer(newTerminalLayout(120, 40))
|
||||
in := append([]byte("\x1b[5;1H"), bytesRepeat('x', 91)...)
|
||||
got := string(vr.Render(in))
|
||||
if strings.Count(got, "x") != 91 {
|
||||
t.Fatalf("91 'x' glyphs from col 1 should all be emitted: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewportRendererDropsUTF8GlyphPastViewport(t *testing.T) {
|
||||
// A 3-byte UTF-8 glyph (U+2500 BOX DRAWINGS LIGHT HORIZONTAL) starting
|
||||
// past the viewport must be dropped as a unit — leaking even one
|
||||
// continuation byte would feed a malformed sequence to the host.
|
||||
vr := newViewportRenderer(newTerminalLayout(120, 40))
|
||||
got := string(vr.Render([]byte("\x1b[5;95H─x")))
|
||||
if strings.Contains(got, "─") {
|
||||
t.Fatalf("UTF-8 glyph past viewport should be dropped: got %q", got)
|
||||
}
|
||||
if strings.Contains(got, "x") {
|
||||
t.Fatalf("trailing ASCII past viewport should also be dropped: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewportRendererFlagsRIAsScrolling(t *testing.T) {
|
||||
// Reproduces the sidebar-gap bug: codex emits `\x1b[1;1H` followed
|
||||
// by 8× `\x1bM` (RI) on startup. RI at the top of the host scroll
|
||||
|
||||
Reference in New Issue
Block a user