diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 65e14dc..7305178 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -702,6 +702,10 @@ target_include_directories(step124_test PRIVATE src) target_link_libraries(step124_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step125_test tests/step125_test.cpp) +target_include_directories(step125_test PRIVATE src) +target_link_libraries(step125_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index b96c1ba..fda55c9 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -157,6 +157,7 @@ struct EditorState { MockWebSocketTransport* agentTransport = nullptr; int agentPort = 8765; std::vector agentLog; + std::map agentMutationPermissions; // Custom editor widget state bool showWhitespace = false; @@ -713,12 +714,25 @@ struct EditorState { auto transport = std::make_unique(); agentTransport = transport.get(); agentServer = std::make_unique(std::move(transport)); - agentServer->setRequestHandler([this](const json& request) { - return processAgentRequest(request); + agentServer->setRequestHandler([this](const json& request, + const std::string& sessionId) { + return processAgentRequest(request, sessionId); }); agentServer->setSessionEventCallback([this](const AgentSession& s, const std::string& event) { logAgentEvent("Agent " + s.sessionId + " " + event); + if (event == "connected") { + agentMutationPermissions[s.sessionId] = false; + } else if (event == "disconnected") { + agentMutationPermissions.erase(s.sessionId); + } + }); + agentServer->setRequestLogCallback([this](const std::string& sid, + const json& req, + const json& res) { + std::string method = req.value("method", ""); + bool ok = !res.contains("error"); + logAgentEvent("RPC " + sid + " " + method + (ok ? " ok" : " error")); }); if (agentServer->start(agentPort)) { logAgentEvent("Agent server started on port " + std::to_string(agentPort)); @@ -767,14 +781,12 @@ struct EditorState { return out; } - json processAgentRequest(const json& request) { + json processAgentRequest(const json& request, const std::string& sessionId) { 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"}}; @@ -794,6 +806,11 @@ struct EditorState { } if (method == "applyMutation") { + auto permIt = agentMutationPermissions.find(sessionId); + if (permIt == agentMutationPermissions.end() || !permIt->second) { + response["error"] = {{"code", -32030}, {"message", "Mutation not permitted"}}; + return response; + } if (!active() || !isStructured()) { response["error"] = {{"code", -32000}, {"message", "No structured buffer"}}; return response; diff --git a/editor/src/WebSocketServer.h b/editor/src/WebSocketServer.h index b33aa64..f94ed32 100644 --- a/editor/src/WebSocketServer.h +++ b/editor/src/WebSocketServer.h @@ -36,9 +36,14 @@ struct AgentSession { }; // Callback types -using JsonRpcHandler = std::function; +using JsonRpcHandler = + std::function; using SessionEventCallback = std::function; +using RequestLogCallback = + std::function; // --------------------------------------------------------------------------- // WebSocketTransport — abstract transport interface @@ -119,6 +124,10 @@ public: sessionEventCallback_ = std::move(cb); } + void setRequestLogCallback(RequestLogCallback cb) { + requestLogCallback_ = std::move(cb); + } + // --- session queries --- const AgentSession* getSession(const std::string& sessionId) const { @@ -188,6 +197,13 @@ private: }; } + if (requestLogCallback_) { + try { + requestLogCallback_(sessionId, json::parse(message), response); + } catch (...) { + // Ignore logging parse errors + } + } transport_->sendMessage(sessionId, response.dump()); } @@ -252,7 +268,7 @@ private: } else if (requestHandler_) { // Delegate to the orchestrator's processRequest - response = requestHandler_(request); + response = requestHandler_(request, sessionId); // Ensure jsonrpc and id are set response["jsonrpc"] = "2.0"; if (!response.contains("id")) @@ -287,6 +303,7 @@ private: std::map sessions_; JsonRpcHandler requestHandler_; SessionEventCallback sessionEventCallback_; + RequestLogCallback requestLogCallback_; int port_ = 0; }; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 112d893..917df47 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -1656,8 +1656,27 @@ int main(int, char**) { 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::TextUnformatted(label.c_str()); + ImGui::SameLine(260.0f); + bool canMutate = state.agentMutationPermissions[s.sessionId]; + std::string checkId = "Can mutate##" + s.sessionId; + if (ImGui::Checkbox(checkId.c_str(), &canMutate)) { + state.agentMutationPermissions[s.sessionId] = canMutate; + } + ImGui::SameLine(420.0f); + ImGui::Text("Msgs: %d", s.messageCount); + ImGui::SameLine(520.0f); + ImGui::Text("Last: %llu", (unsigned long long)s.lastMessageAtMs); + ImGui::SameLine(680.0f); + std::string btnId = "Disconnect##" + s.sessionId; + if (ImGui::Button(btnId.c_str())) { + if (state.agentTransport) { + state.agentTransport->simulateDisconnect(s.sessionId); + } else { + state.logAgentEvent("STUB: disconnect not supported for real transport"); + } + } + ImGui::Separator(); } } diff --git a/editor/tests/step124_test.cpp b/editor/tests/step124_test.cpp index a02ce50..1d8f8c6 100644 --- a/editor/tests/step124_test.cpp +++ b/editor/tests/step124_test.cpp @@ -20,7 +20,7 @@ int main() { MockWebSocketTransport* raw = transport.get(); WebSocketAgentServer server(std::move(transport)); - server.setRequestHandler([](const json& req) { + server.setRequestHandler([](const json& req, const std::string&) { json res; res["result"] = {{"ok", true}, {"echo", req.value("method", "")}}; return res; diff --git a/editor/tests/step125_test.cpp b/editor/tests/step125_test.cpp new file mode 100644 index 0000000..d5a4ef9 --- /dev/null +++ b/editor/tests/step125_test.cpp @@ -0,0 +1,41 @@ +// Step 125 TDD Test: Request log callback +#include "WebSocketServer.h" +#include + +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* raw = transport.get(); + WebSocketAgentServer server(std::move(transport)); + + bool logCalled = false; + std::string lastMethod; + server.setRequestLogCallback([&](const std::string&, + const json& req, + const json&) { + logCalled = true; + lastMethod = req.value("method", ""); + }); + + server.start(9001); + std::string sid = raw->simulateConnect(); + json msg = {{"jsonrpc","2.0"},{"id",1},{"method","ping"}}; + raw->simulateMessage(sid, msg.dump()); + expect(logCalled, "request log callback", passed, failed); + expect(lastMethod == "ping", "method logged", passed, failed); + + std::cout << "\n=== Step 125 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint4_plan.md b/sprint4_plan.md index ce05673..0d34df0 100644 --- a/sprint4_plan.md +++ b/sprint4_plan.md @@ -427,7 +427,7 @@ External tool integration and agent connectivity. Agents receive LSP diagnostics and Whetstone annotations as context. *Wires:* `WebSocketServer.h` -- [ ] **Step 125: Connected agents panel** +- [x] **Step 125: Connected agents panel** "Agents" tab shows currently connected agents: name, session ID, last activity. Manual disconnect button. Live activity log showing JSON-RPC method calls and results. Agents can be granted/revoked mutation permissions.