feat(chat): MCP server + WebSocket streaming, replace XML actions

- Backend: kanban binary gana subcomando `kanban mcp` que actua como MCP
  server via stdio. Tools = mismo set que executeTool (14). El subprocess
  llama de vuelta al backend via /api/tool/{name} con token interno.
- Backend: nuevo endpoint POST /api/tool/{name} (auth: X-Internal-Token).
- Backend: chat.go refactor — POST /api/chat reemplazado por GET
  /api/chat/ws (WebSocket). Lanza claude -p con --output-format stream-json
  --verbose --mcp-config y reenvia eventos (delta/tool_use/tool_result/
  result/done/error) como mensajes JSON al cliente.
- Backend: usa funciones nuevas del registry claude_stream_go_core (spawn
  + parser NDJSON) y mcp_server_stdio_go_infra (JSON-RPC stdio).
- Frontend: streamChat sobre WebSocket. ChatPanel renderiza deltas en
  vivo, chips para tool_use, badges teal/red para tool_result.
- Borrado: extractActions, actionsBlockMarker, XML system prompt.
- Tests: 7 nuevos en backend (chat_ws_test.go + endpoint /api/tool).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 14:54:38 +02:00
parent 9e333b0e3e
commit ce49fdf9ff
14 changed files with 2175 additions and 1493 deletions
-31
View File
@@ -340,37 +340,6 @@ func TestExecuteTool_Unknown(t *testing.T) {
mustErr(t, res, "unknown tool")
}
// --- extractActions ---
func TestExtractActions(t *testing.T) {
cases := []struct {
name string
in string
want string
stripOK string
found bool
}{
{"with block", "Hola\n<actions>[{\"tool\":\"x\"}]</actions>\nHecho", `[{"tool":"x"}]`, "Hola\nHecho", true},
{"only block", "<actions>[]</actions>", `[]`, "", true},
{"no block", "Solo texto", "", "Solo texto", false},
{"unclosed", "<actions>foo", "", "<actions>foo", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, stripped, found := extractActions(c.in)
if found != c.found {
t.Fatalf("found = %v want %v", found, c.found)
}
if got != c.want {
t.Fatalf("got %q want %q", got, c.want)
}
if stripped != c.stripOK {
t.Fatalf("stripped = %q want %q", stripped, c.stripOK)
}
})
}
}
// --- chat logger ---
func TestChatLogger_AppendsJSONLines(t *testing.T) {