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

@@ -237,6 +237,9 @@ func (s *Server) SetHost(h ToolHost) {
// dispatch routes a single JSON-RPC request. callerID is the ID of the
// process that owns this connection (resolved at greeting time).
// Returns nil for notifications (no id present), which tells the caller
// to skip writing a response. Otherwise returns a complete JSON-RPC
// reply ready to send.
func (s *Server) dispatch(callerID string, req []byte) []byte {
var msg struct {
JSONRPC string `json:"jsonrpc"`
@@ -247,14 +250,37 @@ func (s *Server) dispatch(callerID string, req []byte) []byte {
if err := json.Unmarshal(req, &msg); err != nil {
return jsonRPCError(nil, codeParseError, "parse error: "+err.Error(), nil)
}
isNotification := len(msg.ID) == 0 || string(msg.ID) == "null"
// MCP protocol-level methods (initialize, tools/list, tools/call,
// ping, notifications) run before legacy direct-tool dispatch so
// real MCP clients can hand-shake even when host isn't ready yet
// (initialize doesn't touch the host).
if result, handled, code, errMsg, data := s.handleProtocolMethod(callerID, msg.Method, msg.Params, isNotification); handled {
if isNotification {
return nil
}
if errMsg != "" {
return jsonRPCError(msg.ID, code, errMsg, data)
}
return jsonRPCResult(msg.ID, result)
}
s.mu.Lock()
host := s.host
s.mu.Unlock()
if host == nil {
if isNotification {
return nil
}
return jsonRPCError(msg.ID, codeInternal, "patterm: tool host not initialized", nil)
}
result, code, errMsg, data := callTool(host, callerID, msg.Method, msg.Params)
if isNotification {
return nil
}
if errMsg != "" {
return jsonRPCError(msg.ID, code, errMsg, data)
}