Step 124: wire agent server and agents panel

This commit is contained in:
Bill
2026-02-09 15:17:12 -07:00
parent 95d15b4c12
commit a1c4572d7a
5 changed files with 243 additions and 1 deletions

View File

@@ -697,6 +697,11 @@ add_executable(step123_test tests/step123_test.cpp)
target_include_directories(step123_test PRIVATE src)
target_link_libraries(step123_test PRIVATE imgui::imgui)
add_executable(step124_test tests/step124_test.cpp)
target_include_directories(step124_test PRIVATE src)
target_link_libraries(step124_test PRIVATE
nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -41,6 +41,7 @@
#include "SessionManager.h"
#include "ZoomUtils.h"
#include "TerminalPanel.h"
#include "WebSocketServer.h"
#include "IncrementalOptimizer.h"
#include "ast/Serialization.h"
#include "ast/Generator.h"
@@ -64,6 +65,8 @@
#include <unordered_map>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// ---------------------------------------------------------------------------
// Forward declarations for utility functions defined in EditorUtils.h
// (used inside EditorState inline method bodies)
@@ -150,6 +153,10 @@ struct EditorState {
bool hasRunResult = false;
int lastRunExitCode = 0;
std::string lastRunCommand;
std::unique_ptr<WebSocketAgentServer> agentServer;
MockWebSocketTransport* agentTransport = nullptr;
int agentPort = 8765;
std::vector<std::string> agentLog;
// Custom editor widget state
bool showWhitespace = false;
@@ -699,6 +706,153 @@ struct EditorState {
loadRecentFiles();
loadSettingsFromDisk();
registerCommands();
initAgentServer();
}
void initAgentServer() {
auto transport = std::make_unique<MockWebSocketTransport>();
agentTransport = transport.get();
agentServer = std::make_unique<WebSocketAgentServer>(std::move(transport));
agentServer->setRequestHandler([this](const json& request) {
return processAgentRequest(request);
});
agentServer->setSessionEventCallback([this](const AgentSession& s,
const std::string& event) {
logAgentEvent("Agent " + s.sessionId + " " + event);
});
if (agentServer->start(agentPort)) {
logAgentEvent("Agent server started on port " + std::to_string(agentPort));
} else {
logAgentEvent("Agent server failed to start on port " + std::to_string(agentPort));
}
}
void shutdownAgentServer() {
if (agentServer) agentServer->stop();
}
void logAgentEvent(const std::string& msg) {
agentLog.push_back(msg);
}
json buildDiagnosticsJson() const {
json out;
json lspArr = json::array();
auto diags = lsp ? lsp->getDiagnostics() : std::vector<LSPClient::Diagnostic>{};
for (const auto& d : diags) {
lspArr.push_back({
{"uri", d.uri},
{"message", d.message},
{"severity", d.severity},
{"range", {
{"start", {{"line", d.range.start.line}, {"character", d.range.start.character}}},
{"end", {{"line", d.range.end.line}, {"character", d.range.end.character}}}
}}
});
}
json whetArr = json::array();
for (const auto& d : whetstoneDiagnostics) {
whetArr.push_back({
{"uri", d.uri},
{"message", d.message},
{"severity", d.severity},
{"line", d.line},
{"character", d.character}
});
}
out["lsp"] = lspArr;
out["whetstone"] = whetArr;
return out;
}
json processAgentRequest(const json& request) {
json response;
response["jsonrpc"] = "2.0";
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
std::string method = request.value("method", "");
logAgentEvent("Agent RPC: " + method);
if (method == "getAST") {
if (!active() || !isStructured()) {
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
return response;
}
Module* ast = activeAST();
if (!ast) {
response["error"] = {{"code", -32001}, {"message", "AST unavailable"}};
return response;
}
response["result"] = {
{"ast", toJson(ast)},
{"annotationCount", countAnnotationNodes(ast)},
{"diagnostics", buildDiagnosticsJson()}
};
return response;
}
if (method == "applyMutation") {
if (!active() || !isStructured()) {
response["error"] = {{"code", -32000}, {"message", "No structured buffer"}};
return response;
}
auto params = request.contains("params") ? request["params"] : json::object();
std::string type = params.value("type", "");
Module* ast = mutationAST();
if (!ast) {
response["error"] = {{"code", -32001}, {"message", "AST unavailable"}};
return response;
}
ASTMutationAPI mut;
mut.setRoot(ast);
ASTMutationAPI::MutationResult res;
if (type == "setProperty") {
res = mut.setProperty(params.value("nodeId", ""),
params.value("property", ""),
params.value("value", ""));
} else if (type == "updateNode") {
std::map<std::string, std::string> props;
if (params.contains("properties")) {
for (auto& [k, v] : params["properties"].items()) {
props[k] = v.get<std::string>();
}
}
res = mut.updateNode(params.value("nodeId", ""), props);
} else if (type == "deleteNode") {
res = mut.deleteNode(params.value("nodeId", ""));
} else if (type == "insertNode") {
ASTNode* node = nullptr;
if (params.contains("node")) {
node = fromJson(params["node"]);
}
res = mut.insertNode(params.value("parentId", ""),
params.value("role", ""), node);
if (!res.success && node) {
deleteTree(node);
}
} else {
response["error"] = {{"code", -32602}, {"message", "Unknown mutation type"}};
return response;
}
if (!res.success) {
response["error"] = {{"code", -32010}, {"message", res.error}};
return response;
}
applyOrchestratorToActive();
response["result"] = {
{"success", true},
{"warning", res.warning}
};
return response;
}
response["error"] = {{"code", -32601}, {"message", "Method not found"}};
return response;
}
void syncOrchestratorFromActive() {

View File

@@ -1641,6 +1641,40 @@ int main(int, char**) {
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Agents")) {
ImGui::PushFont(monoFont);
bool running = state.agentServer && state.agentServer->isRunning();
ImGui::Text("Server: %s", running ? "Running" : "Stopped");
ImGui::SameLine(0, 20);
ImGui::Text("Port: %d", state.agentPort);
ImGui::Separator();
ImGui::TextUnformatted("Active Sessions");
if (!state.agentServer || state.agentServer->getActiveSessionCount() == 0) {
ImGui::TextDisabled("(none)");
} else {
for (const auto& s : state.agentServer->getActiveSessions()) {
std::string label = s.sessionId;
if (!s.agentName.empty()) label += " (" + s.agentName + ")";
label += " - msgs: " + std::to_string(s.messageCount);
ImGui::BulletText("%s", label.c_str());
}
}
ImGui::Separator();
ImGui::TextUnformatted("Activity Log");
ImGui::BeginChild("##agentLog", ImVec2(0, 0), false);
for (const auto& line : state.agentLog) {
ImGui::TextUnformatted(line.c_str());
}
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20)
ImGui::SetScrollHereY(1.0f);
ImGui::EndChild();
ImGui::PopFont();
ImGui::EndTabItem();
}
// Problems (LSP diagnostics)
if (ImGui::BeginTabItem("Problems")) {
ImGui::PushFont(monoFont);
@@ -2284,6 +2318,7 @@ int main(int, char**) {
if (iniData && iniSize > 0) {
state.saveSession(std::string(iniData, iniSize));
}
state.shutdownAgentServer();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();

View File

@@ -0,0 +1,48 @@
// Step 124 TDD Test: Agent server wiring with mock transport
#include "WebSocketServer.h"
#include <iostream>
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
++failed;
}
}
int main() {
int passed = 0;
int failed = 0;
auto transport = std::make_unique<MockWebSocketTransport>();
MockWebSocketTransport* raw = transport.get();
WebSocketAgentServer server(std::move(transport));
server.setRequestHandler([](const json& req) {
json res;
res["result"] = {{"ok", true}, {"echo", req.value("method", "")}};
return res;
});
expect(server.start(8765), "server starts", passed, failed);
std::string sid = raw->simulateConnect();
expect(!sid.empty(), "session id assigned", passed, failed);
json ping = {{"jsonrpc","2.0"},{"id",1},{"method","ping"}};
raw->simulateMessage(sid, ping.dump());
std::string reply = raw->getLastSentMessage(sid);
expect(reply.find("pong") != std::string::npos, "ping response", passed, failed);
json custom = {{"jsonrpc","2.0"},{"id",2},{"method","getAST"}};
raw->simulateMessage(sid, custom.dump());
std::string reply2 = raw->getLastSentMessage(sid);
expect(reply2.find("\"ok\":true") != std::string::npos, "custom handler", passed, failed);
raw->simulateDisconnect(sid);
expect(server.getActiveSessionCount() == 0, "session disconnect", passed, failed);
std::cout << "\n=== Step 124 Results: " << passed << " passed, " << failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -420,7 +420,7 @@ External tool integration and agent connectivity.
Status bar shows "Running..." / exit code. Ctrl+Shift+B for build.
*Modifies:* `main.cpp`
- [ ] **Step 124: Wire WebSocket agent server**
- [x] **Step 124: Wire WebSocket agent server**
Start `WebSocketAgentServer` when the editor launches (configurable port).
External AI agents can connect, query the AST, and apply mutations via
JSON-RPC. Agent activity logged in "Agents" tab in the bottom panel.