Land staged session/MCP/chrome work + sidebar clear-J fix

This batches the in-flight [Unreleased] block from CHANGELOG.md into a
single commit. Highlights:

- Real MCP protocol layer (initialize / tools/list / tools/call) so
  vendor MCP clients can complete the handshake against the per-PID
  socket. Legacy direct-dispatch preserved for the harness.
- New mcp_injection kinds — cli_override for codex, config_env for
  opencode — joining the existing env-var and config_file paths so
  patterm can slot into more agents without touching their real
  config or auth.
- Ctrl+A/D and Ctrl+W/S focus navigation across tabs and intra-tab
  process lists, recognised in legacy / kitty CSI u / xterm
  modifyOtherKeys encodings.
- Palette macros (sw / k / sp ) and reordering so open sessions
  surface above spawn-new entries.
- Two-row tab bar, sidebar/tabbar/status chrome cache, viewport-wipe
  on agent spawn, CR-terminated orchestrator injections, and split-
  Enter PTY writes so paste-detecting TUIs see Enter as a key event.

Also fixes the bug logged in TODO: claude's Ctrl+O tool-call expansion
emits CSI 0 J, which the viewport renderer was forwarding verbatim —
wiping the sidebar to the right of the cursor and leaving the chrome
cache convinced nothing had changed. CSI 0 J and CSI 1 J are now
translated into per-row ECH sequences clamped to the viewport, same
as CSI 2 J and CSI K already were.

Agent guides (CLAUDE.md / AGENTS.md) now spell out the
TODO->CHANGELOG workflow so completed items land in the changelog
rather than as ticked entries left behind in TODO.
This commit is contained in:
2026-05-14 19:09:35 +01:00
parent 7649587f9a
commit 3622c41fd0
25 changed files with 1951 additions and 163 deletions

View File

@@ -429,24 +429,42 @@ func (c *Child) teardownPTY() {
// pane. SPEC §6: the user's first keystroke flips ownership.
func (c *Child) InjectAsUser(b []byte) error {
c.SetOwner(OwnerUser)
pty := c.PTY()
if pty == nil {
return errors.New("child has no pty")
}
_, err := pty.Write(b)
return err
return c.writeInput(b)
}
// InjectAsOrchestrator is the path send_message / initial_prompt /
// timer_wait writes take. Ownership flips back to orchestrator. SPEC §6.
func (c *Child) InjectAsOrchestrator(b []byte) error {
c.SetOwner(OwnerOrchestrator)
return c.writeInput(b)
}
// writeInput is the shared PTY write path used by both injection
// flavours. Each Enter byte (CR or LF) is split onto its own write
// with a brief delay so TUI agents with paste-detection (claude,
// codex, opencode) don't coalesce a trailing CR into the text that
// preceded it. Without the split, `pty.Write([]byte("hello\r"))`
// arrives at the agent as one read() and gets treated as multi-line
// pasted content rather than "key Enter".
func (c *Child) writeInput(b []byte) error {
pty := c.PTY()
if pty == nil {
return errors.New("child has no pty")
}
_, err := pty.Write(b)
return err
pieces := splitOnEnter(b)
if len(pieces) <= 1 {
_, err := pty.Write(b)
return err
}
for i, piece := range pieces {
if i > 0 {
time.Sleep(15 * time.Millisecond)
}
if _, err := pty.Write(piece); err != nil {
return err
}
}
return nil
}
func mintIdentity() string {