diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index aeafb88..6c00eba 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4540,4 +4540,13 @@ target_link_libraries(step628_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step629_test tests/step629_test.cpp) +target_include_directories(step629_test PRIVATE src) +target_link_libraries(step629_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) diff --git a/editor/src/AgentChatContextInjector.h b/editor/src/AgentChatContextInjector.h new file mode 100644 index 0000000..68ee7c8 --- /dev/null +++ b/editor/src/AgentChatContextInjector.h @@ -0,0 +1,54 @@ +#pragma once +// Step 629: Project context auto-injection + +#include "AgentChatPanelModel.h" + +#include + +struct AgentChatRuntimeSnapshot { + std::string activeFile = "(no-active-buffer)"; + std::string language = "python"; + int astNodeCount = 0; + int openAnnotationCount = 0; + std::string workspacePath = "."; +}; + +struct AgentChatContextBlock { + std::string activeFile; + std::string language; + int astNodeCount = 0; + int openAnnotationCount = 0; + std::string workspacePath; +}; + +class AgentChatContextInjector { +public: + static AgentChatContextBlock fromSnapshot(const AgentChatRuntimeSnapshot& snapshot) { + AgentChatContextBlock block; + block.activeFile = snapshot.activeFile; + block.language = snapshot.language; + block.workspacePath = snapshot.workspacePath; + block.astNodeCount = snapshot.astNodeCount; + block.openAnnotationCount = snapshot.openAnnotationCount; + return block; + } + + static std::string toPromptText(const AgentChatContextBlock& block) { + return "Context:\n" + "- activeFile: " + block.activeFile + "\n" + "- language: " + block.language + "\n" + "- astNodeCount: " + std::to_string(block.astNodeCount) + "\n" + "- openAnnotationCount: " + std::to_string(block.openAnnotationCount) + "\n" + "- workspacePath: " + block.workspacePath; + } + + static void inject(AgentChatState* chat, + const AgentChatRuntimeSnapshot& snapshot, + const std::string& timestamp) { + if (!chat) return; + AgentChatContextBlock block = fromSnapshot(snapshot); + chat->systemContext = toPromptText(block); + chat->messages.push_back({AgentChatRole::Assistant, chat->systemContext, timestamp}); + chat->autoScroll = true; + } +}; diff --git a/editor/src/AgentChatPanelModel.h b/editor/src/AgentChatPanelModel.h index b004ea0..de29fb3 100644 --- a/editor/src/AgentChatPanelModel.h +++ b/editor/src/AgentChatPanelModel.h @@ -26,6 +26,7 @@ struct AgentChatState { std::vector toolCalls; std::vector mutationPreviews; std::vector mutationApprovals; + std::string systemContext; std::string draftInput; bool autoScroll = true; bool open = false; diff --git a/editor/src/panels/AgentChatPanel.h b/editor/src/panels/AgentChatPanel.h index 2f59a31..980e246 100644 --- a/editor/src/panels/AgentChatPanel.h +++ b/editor/src/panels/AgentChatPanel.h @@ -1,9 +1,17 @@ #pragma once #include "../AgentChatPanelModel.h" +#include "../AgentChatContextInjector.h" #include "../EditorState.h" #include "../EditorUtils.h" +static int countChatContextNodes(const ASTNode* node) { + if (!node) return 0; + int total = 1; + for (const auto* child : node->allChildren()) total += countChatContextNodes(child); + return total; +} + static void renderAgentChatPanel(EditorState& state) { if (!state.ui.showAgentChatPanel) return; if (!ImGui::Begin("Agent Chat", &state.ui.showAgentChatPanel)) { @@ -12,6 +20,18 @@ static void renderAgentChatPanel(EditorState& state) { } AgentChatState& chat = state.agent.chat; + if (chat.systemContext.empty()) { + AgentChatRuntimeSnapshot snapshot; + if (state.activeBuffer) { + snapshot.activeFile = state.activeBuffer->path; + snapshot.language = state.activeBuffer->language; + } + snapshot.workspacePath = state.workspaceRoot.empty() ? "." : state.workspaceRoot; + Module* ast = state.activeAST(); + snapshot.astNodeCount = countChatContextNodes(ast); + snapshot.openAnnotationCount = countAnnotationNodes(ast); + AgentChatContextInjector::inject(&chat, snapshot, "context"); + } 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)); diff --git a/editor/tests/step629_test.cpp b/editor/tests/step629_test.cpp new file mode 100644 index 0000000..c057ec7 --- /dev/null +++ b/editor/tests/step629_test.cpp @@ -0,0 +1,146 @@ +// Step 629: Project context auto-injection (12 tests) + +#include "AgentChatContextInjector.h" + +#include + +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_from_editor_without_active_buffer_uses_defaults() { + TEST(from_editor_without_active_buffer_uses_defaults); + AgentChatRuntimeSnapshot snapshot; + snapshot.workspacePath = "/tmp/ws"; + auto block = AgentChatContextInjector::fromSnapshot(snapshot); + CHECK(block.activeFile == "(no-active-buffer)", "active file mismatch"); + CHECK(block.language == "python", "language mismatch"); + CHECK(block.workspacePath == "/tmp/ws", "workspace mismatch"); + PASS(); +} + +void test_from_editor_uses_active_buffer_path_and_language() { + TEST(from_editor_uses_active_buffer_path_and_language); + AgentChatRuntimeSnapshot snapshot; + snapshot.activeFile = "src/main.cpp"; + snapshot.language = "cpp"; + auto block = AgentChatContextInjector::fromSnapshot(snapshot); + CHECK(block.activeFile == "src/main.cpp", "active file mismatch"); + CHECK(block.language == "cpp", "language mismatch"); + PASS(); +} + +void test_from_editor_uses_dot_workspace_when_empty() { + TEST(from_editor_uses_dot_workspace_when_empty); + AgentChatRuntimeSnapshot snapshot; + auto block = AgentChatContextInjector::fromSnapshot(snapshot); + CHECK(block.workspacePath == ".", "workspace fallback mismatch"); + PASS(); +} + +void test_from_editor_counts_zero_without_ast() { + TEST(from_editor_counts_zero_without_ast); + AgentChatRuntimeSnapshot snapshot; + auto block = AgentChatContextInjector::fromSnapshot(snapshot); + CHECK(block.astNodeCount == 0, "ast count should be zero"); + CHECK(block.openAnnotationCount == 0, "annotation count should be zero"); + PASS(); +} + +void test_prompt_text_contains_active_file_line() { + TEST(prompt_text_contains_active_file_line); + AgentChatContextBlock block; + block.activeFile = "file.cpp"; + block.language = "cpp"; + block.workspacePath = "/tmp/ws"; + std::string text = AgentChatContextInjector::toPromptText(block); + CHECK(text.find("activeFile: file.cpp") != std::string::npos, "activeFile line missing"); + PASS(); +} + +void test_prompt_text_contains_language_line() { + TEST(prompt_text_contains_language_line); + AgentChatContextBlock block; + block.language = "rust"; + std::string text = AgentChatContextInjector::toPromptText(block); + CHECK(text.find("language: rust") != std::string::npos, "language line missing"); + PASS(); +} + +void test_prompt_text_contains_ast_count_line() { + TEST(prompt_text_contains_ast_count_line); + AgentChatContextBlock block; + block.astNodeCount = 17; + std::string text = AgentChatContextInjector::toPromptText(block); + CHECK(text.find("astNodeCount: 17") != std::string::npos, "ast count line missing"); + PASS(); +} + +void test_prompt_text_contains_annotation_count_line() { + TEST(prompt_text_contains_annotation_count_line); + AgentChatContextBlock block; + block.openAnnotationCount = 3; + std::string text = AgentChatContextInjector::toPromptText(block); + CHECK(text.find("openAnnotationCount: 3") != std::string::npos, "annotation count line missing"); + PASS(); +} + +void test_prompt_text_contains_workspace_line() { + TEST(prompt_text_contains_workspace_line); + AgentChatContextBlock block; + block.workspacePath = "/home/user/proj"; + std::string text = AgentChatContextInjector::toPromptText(block); + CHECK(text.find("workspacePath: /home/user/proj") != std::string::npos, "workspace line missing"); + PASS(); +} + +void test_inject_sets_system_context() { + TEST(inject_sets_system_context); + AgentChatRuntimeSnapshot snapshot; + AgentChatState chat; + AgentChatContextInjector::inject(&chat, snapshot, "12:30"); + CHECK(!chat.systemContext.empty(), "systemContext should be set"); + PASS(); +} + +void test_inject_appends_assistant_message() { + TEST(inject_appends_assistant_message); + AgentChatRuntimeSnapshot snapshot; + AgentChatState chat; + AgentChatContextInjector::inject(&chat, snapshot, "12:31"); + CHECK(chat.messages.size() == 1, "one message expected"); + CHECK(chat.messages[0].role == AgentChatRole::Assistant, "role should be assistant"); + PASS(); +} + +void test_inject_enables_auto_scroll() { + TEST(inject_enables_auto_scroll); + AgentChatRuntimeSnapshot snapshot; + AgentChatState chat; + chat.autoScroll = false; + AgentChatContextInjector::inject(&chat, snapshot, "12:32"); + CHECK(chat.autoScroll, "autoScroll should be enabled"); + PASS(); +} + +int main() { + std::cout << "Step 629: Project context auto-injection\n"; + + test_from_editor_without_active_buffer_uses_defaults(); // 1 + test_from_editor_uses_active_buffer_path_and_language();// 2 + test_from_editor_uses_dot_workspace_when_empty(); // 3 + test_from_editor_counts_zero_without_ast(); // 4 + test_prompt_text_contains_active_file_line(); // 5 + test_prompt_text_contains_language_line(); // 6 + test_prompt_text_contains_ast_count_line(); // 7 + test_prompt_text_contains_annotation_count_line(); // 8 + test_prompt_text_contains_workspace_line(); // 9 + test_inject_sets_system_context(); // 10 + test_inject_appends_assistant_message(); // 11 + test_inject_enables_auto_scroll(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index a1b01ad..fe83111 100644 --- a/progress.md +++ b/progress.md @@ -12595,3 +12595,37 @@ accept decision, and final code state update. - `editor/src/Phase37aIntegration.h` (`51` <= `600`) - `editor/tests/step628_test.cpp` within test-file size guidance (`93` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 629: Project context auto-injection +**Status:** PASS (12/12 tests) + +Adds automatic chat-context injection at chat start with: +active file, language, AST node count, open annotation count, and workspace path. + +**Files added:** +- `editor/src/AgentChatContextInjector.h` - context block model + prompt injection: + - runtime snapshot-to-context conversion + - prompt-text composition for system context + - chat-state context injection helper +- `editor/tests/step629_test.cpp` - 12 tests covering: + - snapshot/context mapping defaults and field propagation + - prompt text composition content + - injection behavior for system context + assistant message + auto-scroll + +**Files modified:** +- `editor/src/AgentChatPanelModel.h` - added `systemContext` storage field +- `editor/src/panels/AgentChatPanel.h` - auto-injects context snapshot on chat start +- `editor/CMakeLists.txt` - `step629_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step629_test step628_test` - PASS +- `./editor/build-native/step629_test` - PASS (12/12) +- `./editor/build-native/step628_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/AgentChatContextInjector.h` (`54` <= `600`) +- `editor/src/AgentChatPanelModel.h` (`112` <= `600`) +- `editor/src/panels/AgentChatPanel.h` (`126` <= `600`) +- `editor/tests/step629_test.cpp` within test-file size guidance (`146` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`