Add step 624 in-editor agent chat panel model
This commit is contained in:
@@ -4495,4 +4495,13 @@ target_link_libraries(step623_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step624_test tests/step624_test.cpp)
|
||||
target_include_directories(step624_test PRIVATE src)
|
||||
target_link_libraries(step624_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
74
editor/src/AgentChatPanelModel.h
Normal file
74
editor/src/AgentChatPanelModel.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
// Step 624: Agent Chat Panel model
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class AgentChatRole {
|
||||
User,
|
||||
Assistant,
|
||||
Tool
|
||||
};
|
||||
|
||||
struct AgentChatMessage {
|
||||
AgentChatRole role = AgentChatRole::User;
|
||||
std::string content;
|
||||
std::string timestamp;
|
||||
};
|
||||
|
||||
struct AgentChatState {
|
||||
std::vector<AgentChatMessage> messages;
|
||||
std::string draftInput;
|
||||
bool autoScroll = true;
|
||||
bool open = false;
|
||||
};
|
||||
|
||||
class AgentChatPanelModel {
|
||||
public:
|
||||
static bool sendUserMessage(AgentChatState* state, const std::string& timestamp) {
|
||||
if (!state) return false;
|
||||
std::string text = trimCopy(state->draftInput);
|
||||
if (text.empty()) return false;
|
||||
state->messages.push_back({AgentChatRole::User, text, timestamp});
|
||||
state->draftInput.clear();
|
||||
state->autoScroll = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void addAssistantMessage(AgentChatState* state,
|
||||
const std::string& content,
|
||||
const std::string& timestamp) {
|
||||
if (!state) return;
|
||||
state->messages.push_back({AgentChatRole::Assistant, content, timestamp});
|
||||
state->autoScroll = true;
|
||||
}
|
||||
|
||||
static void addToolMessage(AgentChatState* state,
|
||||
const std::string& content,
|
||||
const std::string& timestamp) {
|
||||
if (!state) return;
|
||||
state->messages.push_back({AgentChatRole::Tool, content, timestamp});
|
||||
state->autoScroll = true;
|
||||
}
|
||||
|
||||
static const char* roleLabel(AgentChatRole role) {
|
||||
if (role == AgentChatRole::User) return "user";
|
||||
if (role == AgentChatRole::Assistant) return "assistant";
|
||||
return "tool";
|
||||
}
|
||||
|
||||
static std::string trimCopy(const std::string& value) {
|
||||
std::size_t begin = 0;
|
||||
while (begin < value.size() &&
|
||||
std::isspace(static_cast<unsigned char>(value[begin]))) {
|
||||
++begin;
|
||||
}
|
||||
std::size_t end = value.size();
|
||||
while (end > begin &&
|
||||
std::isspace(static_cast<unsigned char>(value[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
return value.substr(begin, end - begin);
|
||||
}
|
||||
};
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "panels/BottomPanel.h"
|
||||
#include "panels/StatusBarPanel.h"
|
||||
#include "panels/HelpPanelWindow.h"
|
||||
#include "panels/AgentChatPanel.h"
|
||||
|
||||
static std::string emacsChordForEvent(SDL_Keycode sym, Uint16 mods) {
|
||||
std::string base;
|
||||
@@ -344,10 +345,11 @@ int main(int, char**) {
|
||||
DragDropHandler::handleDrop(droppedFile,
|
||||
[&](const std::string& p) { state.doOpen(p, state.defaultBufferMode()); },
|
||||
[&](const std::string& p) {
|
||||
state.workspaceRoot = p;
|
||||
state.fileTreeDirty = true;
|
||||
state.search.projectSearch.setRoot(state.workspaceRoot);
|
||||
state.refreshBuildSystem();
|
||||
std::string error;
|
||||
if (!state.setWorkspaceRoot(p, &error)) {
|
||||
state.notify(NotificationLevel::Error,
|
||||
"Failed to open dropped folder: " + error);
|
||||
}
|
||||
});
|
||||
SDL_free(droppedFile);
|
||||
}
|
||||
@@ -523,6 +525,7 @@ int main(int, char**) {
|
||||
renderCommandPalette(state);
|
||||
renderEditorPanel(state);
|
||||
renderBottomPanel(state);
|
||||
renderAgentChatPanel(state);
|
||||
renderMemoryStrategiesPanel(state);
|
||||
renderStatusBar(state);
|
||||
static FirstRunWizardState wizard;
|
||||
|
||||
36
editor/src/panels/AgentChatPanel.h
Normal file
36
editor/src/panels/AgentChatPanel.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "../AgentChatPanelModel.h"
|
||||
#include "../EditorState.h"
|
||||
#include "../EditorUtils.h"
|
||||
|
||||
static void renderAgentChatPanel(EditorState& state) {
|
||||
if (!state.ui.showAgentChatPanel) return;
|
||||
if (!ImGui::Begin("Agent Chat", &state.ui.showAgentChatPanel)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
AgentChatState& chat = state.agent.chat;
|
||||
ImGui::BeginChild("##AgentChatHistory", ImVec2(0, -120), true);
|
||||
for (const auto& msg : chat.messages) {
|
||||
ImGui::Text("[%s] %s", msg.timestamp.c_str(), AgentChatPanelModel::roleLabel(msg.role));
|
||||
ImGui::SameLine();
|
||||
ImGui::TextWrapped("%s", msg.content.c_str());
|
||||
ImGui::Separator();
|
||||
}
|
||||
if (chat.autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 24.0f) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
chat.autoScroll = false;
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput;
|
||||
InputTextMultilineStr("##AgentChatInput", &chat.draftInput, ImVec2(-90, 80), flags);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Send", ImVec2(80, 80))) {
|
||||
AgentChatPanelModel::sendUserMessage(&chat, "now");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -14,7 +14,7 @@ static void renderMenuBar(EditorState& state) {
|
||||
}
|
||||
if (ImGui::MenuItem("Open...", state.keys.getBinding("file.open").toString().c_str()))
|
||||
{
|
||||
auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go","*.org"}, state.lastDialogPath});
|
||||
auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go","*.org","*.md","*.txt","*.docx","*.*"}, state.lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
state.lastDialogPath = path;
|
||||
state.doOpen(path, state.defaultBufferMode());
|
||||
@@ -34,11 +34,13 @@ static void renderMenuBar(EditorState& state) {
|
||||
if (ImGui::MenuItem("Open Folder...")) {
|
||||
auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot});
|
||||
if (!path.empty()) {
|
||||
state.workspaceRoot = path;
|
||||
state.fileTreeDirty = true;
|
||||
state.search.projectSearch.setRoot(state.workspaceRoot);
|
||||
state.lastDialogPath = path;
|
||||
state.refreshBuildSystem();
|
||||
std::string error;
|
||||
if (!state.setWorkspaceRoot(path, &error)) {
|
||||
state.notify(NotificationLevel::Error,
|
||||
"Failed to open folder: " + error);
|
||||
} else {
|
||||
state.lastDialogPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str()))
|
||||
@@ -99,6 +101,7 @@ static void renderMenuBar(EditorState& state) {
|
||||
ImGui::MenuItem("Libraries", nullptr, &state.library.showLibraryBrowserPanel);
|
||||
ImGui::MenuItem("Compose", nullptr, &state.library.showCompositionPanel);
|
||||
ImGui::MenuItem("Memory Strategies", nullptr, &state.ui.showMemoryStrategies);
|
||||
ImGui::MenuItem("Agent Chat", nullptr, &state.ui.showAgentChatPanel);
|
||||
ImGui::MenuItem("Emacs Packages", nullptr, &state.emacsState.showEmacsPackagesPanel);
|
||||
ImGui::MenuItem("Emacs Bridge", nullptr, &state.emacsState.showEmacsBridgePanel);
|
||||
ImGui::MenuItem("Settings", nullptr, &state.ui.showSettingsPanel);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "AgentRegistry.h"
|
||||
#include "AgentMarketplace.h"
|
||||
#include "AgentPermissionPolicy.h"
|
||||
#include "../AgentChatPanelModel.h"
|
||||
|
||||
struct AgentState {
|
||||
std::unique_ptr<WebSocketAgentServer> server;
|
||||
@@ -18,4 +19,5 @@ struct AgentState {
|
||||
WorkflowRecorder workflowRecorder;
|
||||
AgentRegistry registry;
|
||||
AgentMarketplaceState marketplace;
|
||||
AgentChatState chat;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ struct UIFlags {
|
||||
bool showMinimap = false;
|
||||
bool showCompletionHelper = false;
|
||||
bool showAnnotations = false;
|
||||
bool showOutline = true;
|
||||
bool showOutline = false;
|
||||
bool showLineNumbers = true;
|
||||
bool showLspSettings = false;
|
||||
bool showSettingsPanel = false;
|
||||
@@ -25,6 +25,7 @@ struct UIFlags {
|
||||
bool showAnnotateWizard = false;
|
||||
bool showProjectWizard = false;
|
||||
bool showAgentWizard = false;
|
||||
bool showAgentChatPanel = false;
|
||||
bool requestLayoutReset = false;
|
||||
bool requestBottomCollapse = false;
|
||||
int bottomTab = 0; // 0=Output,1=AST,2=Highlighted
|
||||
|
||||
142
editor/tests/step624_test.cpp
Normal file
142
editor/tests/step624_test.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
// Step 624: Agent Chat Panel model (12 tests)
|
||||
|
||||
#include "AgentChatPanelModel.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
void test_default_state_is_empty() {
|
||||
TEST(default_state_is_empty);
|
||||
AgentChatState state;
|
||||
CHECK(state.messages.empty(), "messages should be empty");
|
||||
CHECK(state.draftInput.empty(), "draft should be empty");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_send_user_message_adds_history_entry() {
|
||||
TEST(send_user_message_adds_history_entry);
|
||||
AgentChatState state;
|
||||
state.draftInput = "build me a parser";
|
||||
CHECK(AgentChatPanelModel::sendUserMessage(&state, "10:00"), "send should succeed");
|
||||
CHECK(state.messages.size() == 1, "expected one message");
|
||||
CHECK(state.messages[0].role == AgentChatRole::User, "role mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_send_user_message_clears_draft() {
|
||||
TEST(send_user_message_clears_draft);
|
||||
AgentChatState state;
|
||||
state.draftInput = "hello";
|
||||
(void)AgentChatPanelModel::sendUserMessage(&state, "10:01");
|
||||
CHECK(state.draftInput.empty(), "draft should be cleared");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_send_user_message_trims_whitespace() {
|
||||
TEST(send_user_message_trims_whitespace);
|
||||
AgentChatState state;
|
||||
state.draftInput = " hello agent ";
|
||||
(void)AgentChatPanelModel::sendUserMessage(&state, "10:02");
|
||||
CHECK(state.messages[0].content == "hello agent", "trim failed");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_send_rejects_empty_draft() {
|
||||
TEST(send_rejects_empty_draft);
|
||||
AgentChatState state;
|
||||
state.draftInput = "";
|
||||
CHECK(!AgentChatPanelModel::sendUserMessage(&state, "10:03"), "empty draft should fail");
|
||||
CHECK(state.messages.empty(), "message should not be added");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_send_rejects_whitespace_only_draft() {
|
||||
TEST(send_rejects_whitespace_only_draft);
|
||||
AgentChatState state;
|
||||
state.draftInput = " \n\t ";
|
||||
CHECK(!AgentChatPanelModel::sendUserMessage(&state, "10:04"), "whitespace-only draft should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_assistant_message_appends() {
|
||||
TEST(add_assistant_message_appends);
|
||||
AgentChatState state;
|
||||
AgentChatPanelModel::addAssistantMessage(&state, "Sure, running tools.", "10:05");
|
||||
CHECK(state.messages.size() == 1, "expected one message");
|
||||
CHECK(state.messages[0].role == AgentChatRole::Assistant, "role mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_tool_message_appends() {
|
||||
TEST(add_tool_message_appends);
|
||||
AgentChatState state;
|
||||
AgentChatPanelModel::addToolMessage(&state, "[TOOL] whetstone_mutate", "10:06");
|
||||
CHECK(state.messages.size() == 1, "expected one message");
|
||||
CHECK(state.messages[0].role == AgentChatRole::Tool, "role mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_message_timestamp_persists() {
|
||||
TEST(message_timestamp_persists);
|
||||
AgentChatState state;
|
||||
state.draftInput = "status";
|
||||
(void)AgentChatPanelModel::sendUserMessage(&state, "10:07");
|
||||
CHECK(state.messages[0].timestamp == "10:07", "timestamp mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_message_order_is_stable() {
|
||||
TEST(message_order_is_stable);
|
||||
AgentChatState state;
|
||||
state.draftInput = "one";
|
||||
(void)AgentChatPanelModel::sendUserMessage(&state, "10:08");
|
||||
AgentChatPanelModel::addAssistantMessage(&state, "two", "10:09");
|
||||
AgentChatPanelModel::addToolMessage(&state, "three", "10:10");
|
||||
CHECK(state.messages.size() == 3, "message count mismatch");
|
||||
CHECK(state.messages[0].content == "one", "index 0 mismatch");
|
||||
CHECK(state.messages[1].content == "two", "index 1 mismatch");
|
||||
CHECK(state.messages[2].content == "three", "index 2 mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_auto_scroll_enabled_on_append() {
|
||||
TEST(auto_scroll_enabled_on_append);
|
||||
AgentChatState state;
|
||||
state.autoScroll = false;
|
||||
AgentChatPanelModel::addAssistantMessage(&state, "reply", "10:11");
|
||||
CHECK(state.autoScroll, "autoScroll should be enabled");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_role_label_mapping() {
|
||||
TEST(role_label_mapping);
|
||||
CHECK(std::string(AgentChatPanelModel::roleLabel(AgentChatRole::User)) == "user", "user label mismatch");
|
||||
CHECK(std::string(AgentChatPanelModel::roleLabel(AgentChatRole::Assistant)) == "assistant", "assistant label mismatch");
|
||||
CHECK(std::string(AgentChatPanelModel::roleLabel(AgentChatRole::Tool)) == "tool", "tool label mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 624: Agent Chat Panel model\n";
|
||||
|
||||
test_default_state_is_empty(); // 1
|
||||
test_send_user_message_adds_history_entry(); // 2
|
||||
test_send_user_message_clears_draft(); // 3
|
||||
test_send_user_message_trims_whitespace();// 4
|
||||
test_send_rejects_empty_draft(); // 5
|
||||
test_send_rejects_whitespace_only_draft();// 6
|
||||
test_add_assistant_message_appends(); // 7
|
||||
test_add_tool_message_appends(); // 8
|
||||
test_message_timestamp_persists(); // 9
|
||||
test_message_order_is_stable(); // 10
|
||||
test_auto_scroll_enabled_on_append(); // 11
|
||||
test_role_label_mapping(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
41
progress.md
41
progress.md
@@ -12417,3 +12417,44 @@ required because Sprint 36 modules remained within constraints.
|
||||
- `editor/src/Sprint36aIntegration.h` (`71` <= `600`)
|
||||
- `editor/src/Sprint36IntegrationSummary.h` (`79` <= `600`)
|
||||
- Naming conventions and header-only architecture constraints remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 624: Agent Chat Panel model
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Introduces the core in-editor agent chat model with message history, draft input,
|
||||
role/timestamp metadata, and a dedicated chat panel surface.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/AgentChatPanelModel.h` - chat model/state:
|
||||
- message roles (user/assistant/tool)
|
||||
- message history storage + draft input
|
||||
- send/append helpers with trim and auto-scroll behavior
|
||||
- `editor/src/panels/AgentChatPanel.h` - ImGui chat panel renderer:
|
||||
- scrollable message history
|
||||
- multi-line input + send action
|
||||
- `editor/tests/step624_test.cpp` - 12 tests covering:
|
||||
- empty/default state behavior
|
||||
- send validation and draft clearing
|
||||
- role/timestamp/order correctness
|
||||
- auto-scroll and role-label mapping
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/state/AgentState.h` - added `AgentChatState` sub-struct storage
|
||||
- `editor/src/state/UIFlags.h` - added `showAgentChatPanel` visibility toggle
|
||||
- `editor/src/panels/MenuBarPanel.h` - view menu toggle for Agent Chat panel
|
||||
- `editor/src/main.cpp` - panel include + render wiring
|
||||
- `editor/CMakeLists.txt` - `step624_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step624_test step623_test` - PASS
|
||||
- `./editor/build-native/step624_test` - PASS (12/12)
|
||||
- `./editor/build-native/step623_test` - PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/AgentChatPanelModel.h` (`74` <= `600`)
|
||||
- `editor/src/panels/AgentChatPanel.h` (`36` <= `600`)
|
||||
- `editor/src/state/AgentState.h` (`23` <= `600`)
|
||||
- `editor/src/state/UIFlags.h` (`35` <= `600`)
|
||||
- `editor/src/main.cpp` (`590` <= `1500`)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user