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

@@ -523,7 +523,12 @@ func encodeInput(args mcp.SendInputArgs) ([]byte, error) {
}
out := []byte(args.Text)
if submit {
out = append(out, '\n')
// CR (`\r`) is what every terminal emits for Enter in raw
// mode, and what TUI agents (claude/codex/…) bind to
// "submit". Sending `\n` here used to land as a literal
// newline inside their textareas, leaving the message
// composed but not sent.
out = append(out, '\r')
}
return out, nil
case "paste":
@@ -635,13 +640,13 @@ func classifySendMessage(caller, target *Child, callerID, message string) (strin
return "", mcp.Errorf("not_related", "send_message: cannot send to self")
}
if caller != nil && target.ParentID == caller.ID {
return "[orchestrator] " + message + "\n", nil
return "[orchestrator] " + message + "\r", nil
}
if caller != nil && caller.ParentID == target.ID {
return fmt.Sprintf("[sub-agent:%s] %s\n", caller.DisplayName(), message), nil
return fmt.Sprintf("[sub-agent:%s] %s\r", caller.DisplayName(), message), nil
}
if caller == nil && target.ParentID == "" {
return "[orchestrator] " + message + "\n", nil
return "[orchestrator] " + message + "\r", nil
}
return "", mcp.Errorf("not_related", "send_message: %q is neither parent nor child of caller (siblings must route through the parent in v1)", target.ID)
}
@@ -670,7 +675,7 @@ func (h *toolHost) TimerWait(callerID string, seconds float64, label string) (st
if !caller.IsLive() {
return
}
line := fmt.Sprintf("[system] Your timer [%s] has completed.\n", label)
line := fmt.Sprintf("[system] Your timer [%s] has completed.\r", label)
_ = caller.InjectAsOrchestrator([]byte(line))
}()
return id, nil