diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index e571748..956fffe 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4567,4 +4567,13 @@ target_link_libraries(step631_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step632_test tests/step632_test.cpp) +target_include_directories(step632_test PRIVATE src) +target_link_libraries(step632_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/AgentTaskSlots.h b/editor/src/AgentTaskSlots.h index 9a64b9a..7e1d714 100644 --- a/editor/src/AgentTaskSlots.h +++ b/editor/src/AgentTaskSlots.h @@ -16,6 +16,10 @@ struct AgentBackgroundTask { std::string description; AgentTaskStatus status = AgentTaskStatus::Pending; int slotIndex = -1; + int startedAtSeconds = 0; + int lastElapsedSeconds = 0; + int toolCallsMade = 0; + std::string currentStep; }; struct AgentTaskSlotsState { @@ -43,6 +47,10 @@ public: } static int assignPendingToSlots(AgentTaskSlotsState* state) { + return assignPendingToSlotsAt(state, 0); + } + + static int assignPendingToSlotsAt(AgentTaskSlotsState* state, int nowSeconds) { if (!state) return 0; int assigned = 0; int running = runningCount(*state); @@ -51,6 +59,9 @@ public: if (task.status != AgentTaskStatus::Pending) continue; task.status = AgentTaskStatus::Running; task.slotIndex = nextAvailableSlot(*state); + if (task.startedAtSeconds == 0 && nowSeconds > 0) { + task.startedAtSeconds = nowSeconds; + } ++running; ++assigned; } @@ -73,6 +84,29 @@ public: return true; } + static bool updateTaskProgress(AgentTaskSlotsState* state, + const std::string& taskId, + int toolCallsMade, + const std::string& currentStep) { + AgentBackgroundTask* task = findMutable(state, taskId); + if (!task) return false; + if (toolCallsMade >= 0) task->toolCallsMade = toolCallsMade; + task->currentStep = currentStep; + return true; + } + + static bool tickTaskElapsed(AgentTaskSlotsState* state, + const std::string& taskId, + int nowSeconds) { + AgentBackgroundTask* task = findMutable(state, taskId); + if (!task) return false; + if (task->startedAtSeconds <= 0 || nowSeconds < task->startedAtSeconds) { + return false; + } + task->lastElapsedSeconds = nowSeconds - task->startedAtSeconds; + return true; + } + static int runningCount(const AgentTaskSlotsState& state) { return countByStatus(state, AgentTaskStatus::Running); } diff --git a/editor/src/AgentTaskStatusOverlay.h b/editor/src/AgentTaskStatusOverlay.h new file mode 100644 index 0000000..4eedc86 --- /dev/null +++ b/editor/src/AgentTaskStatusOverlay.h @@ -0,0 +1,79 @@ +#pragma once +// Step 632: Agent task status overlay model + +#include "AgentTaskSlots.h" + +#include +#include + +struct AgentTaskStatusSummary { + int running = 0; + int pending = 0; + std::string label; + bool hasWork = false; +}; + +struct AgentTaskOverlayRow { + std::string taskId; + std::string description; + std::string status; + int elapsedSeconds = 0; + int toolCallsMade = 0; + std::string currentStep; + bool canCancel = false; + int slotIndex = -1; +}; + +class AgentTaskStatusOverlay { +public: + static AgentTaskStatusSummary buildSummary(const AgentTaskSlotsState& state) { + AgentTaskStatusSummary summary; + summary.running = AgentTaskSlots::runningCount(state); + summary.pending = AgentTaskSlots::pendingCount(state); + summary.hasWork = (summary.running + summary.pending) > 0; + summary.label = "[Agent: " + std::to_string(summary.running) + + " running, " + std::to_string(summary.pending) + " pending]"; + return summary; + } + + static std::vector buildRows(const AgentTaskSlotsState& state, + int nowSeconds) { + std::vector rows; + rows.reserve(state.tasks.size()); + for (const auto& task : state.tasks) { + AgentTaskOverlayRow row; + row.taskId = task.taskId; + row.description = task.description; + row.status = statusText(task.status); + row.toolCallsMade = task.toolCallsMade; + row.currentStep = task.currentStep.empty() ? "-" : task.currentStep; + row.slotIndex = task.slotIndex; + row.canCancel = task.status == AgentTaskStatus::Pending || + task.status == AgentTaskStatus::Running; + row.elapsedSeconds = elapsedSeconds(task, nowSeconds); + rows.push_back(row); + } + return rows; + } + + static bool cancelTask(AgentTaskSlotsState* state, const std::string& taskId) { + return AgentTaskSlots::cancelTask(state, taskId); + } + + static std::string statusText(AgentTaskStatus status) { + if (status == AgentTaskStatus::Pending) return "pending"; + if (status == AgentTaskStatus::Running) return "running"; + if (status == AgentTaskStatus::Completed) return "completed"; + return "cancelled"; + } + +private: + static int elapsedSeconds(const AgentBackgroundTask& task, int nowSeconds) { + if (task.status == AgentTaskStatus::Running && + task.startedAtSeconds > 0 && + nowSeconds >= task.startedAtSeconds) { + return nowSeconds - task.startedAtSeconds; + } + return task.lastElapsedSeconds; + } +}; diff --git a/editor/src/panels/AgentChatPanel.h b/editor/src/panels/AgentChatPanel.h index 6e1ce81..db5941f 100644 --- a/editor/src/panels/AgentChatPanel.h +++ b/editor/src/panels/AgentChatPanel.h @@ -3,8 +3,10 @@ #include "../AgentChatPanelModel.h" #include "../AgentChatContextInjector.h" #include "../AgentChatSessionPersistence.h" +#include "../AgentTaskStatusOverlay.h" #include "../EditorState.h" #include "../EditorUtils.h" +#include static int countChatContextNodes(const ASTNode* node) { if (!node) return 0; @@ -21,6 +23,7 @@ static void renderAgentChatPanel(EditorState& state) { } AgentChatState& chat = state.agent.chat; + AgentTaskSlotsState& taskSlots = state.agent.taskSlots; const std::string projectFile = state.activeBuffer ? state.activeBuffer->path : ""; if (!projectFile.empty() && (!chat.sessionLoaded || chat.loadedProjectFile != projectFile)) { @@ -52,6 +55,31 @@ static void renderAgentChatPanel(EditorState& state) { snapshot.openAnnotationCount = countAnnotationNodes(ast); AgentChatContextInjector::inject(&chat, snapshot, "context"); } + (void)AgentTaskSlots::assignPendingToSlots(&taskSlots); + const int nowSeconds = static_cast(std::time(nullptr)); + const auto overlayRows = AgentTaskStatusOverlay::buildRows(taskSlots, nowSeconds); + + if (!overlayRows.empty() && ImGui::BeginTabBar("##AgentTaskSlotsTabs")) { + for (const auto& row : overlayRows) { + std::string tabName = (row.slotIndex >= 0) + ? ("Slot " + std::to_string(row.slotIndex + 1) + ": " + row.taskId) + : ("Pending: " + row.taskId); + if (ImGui::BeginTabItem(tabName.c_str())) { + ImGui::Text("Task: %s", row.description.c_str()); + ImGui::Text("Status: %s", row.status.c_str()); + ImGui::Text("Elapsed: %ds", row.elapsedSeconds); + ImGui::Text("Tool Calls: %d", row.toolCallsMade); + ImGui::Text("Current Step: %s", row.currentStep.c_str()); + if (row.canCancel && + ImGui::Button(("Cancel##tab_" + row.taskId).c_str())) { + (void)AgentTaskStatusOverlay::cancelTask(&taskSlots, row.taskId); + } + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } + 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)); @@ -135,6 +163,23 @@ static void renderAgentChatPanel(EditorState& state) { } } + if (!overlayRows.empty()) { + ImGui::Separator(); + ImGui::TextUnformatted("Agent Tasks"); + for (const auto& row : overlayRows) { + ImGui::Text("[%s] %s", row.status.c_str(), row.description.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("(elapsed: %ds, tools: %d, step: %s)", + row.elapsedSeconds, + row.toolCallsMade, + row.currentStep.c_str()); + if (row.canCancel && + ImGui::Button(("Cancel##row_" + row.taskId).c_str())) { + (void)AgentTaskStatusOverlay::cancelTask(&taskSlots, row.taskId); + } + } + } + ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput; InputTextMultilineStr("##AgentChatInput", &chat.draftInput, ImVec2(-90, 80), flags); ImGui::SameLine(); diff --git a/editor/src/panels/StatusBarPanel.h b/editor/src/panels/StatusBarPanel.h index 5e2854a..d984648 100644 --- a/editor/src/panels/StatusBarPanel.h +++ b/editor/src/panels/StatusBarPanel.h @@ -1,6 +1,7 @@ #pragma once #include "../EditorState.h" #include "../EditorUtils.h" +#include "../AgentTaskStatusOverlay.h" #include "../ThemeEngine.h" #include #include @@ -160,6 +161,15 @@ static void renderStatusBar(EditorState& state) { ImGui::PopStyleColor(2); } + AgentTaskStatusSummary taskSummary = + AgentTaskStatusOverlay::buildSummary(state.agent.taskSlots); + if (taskSummary.hasWork) { + ImGui::SameLine(0, 10); + if (ImGui::Button(taskSummary.label.c_str())) { + state.ui.showAgentChatPanel = true; + } + } + // Right cluster: Ln/Col, selection, zoom, git int selStart = -1; int selEnd = -1; diff --git a/editor/tests/step632_test.cpp b/editor/tests/step632_test.cpp new file mode 100644 index 0000000..cef5bbd --- /dev/null +++ b/editor/tests/step632_test.cpp @@ -0,0 +1,162 @@ +// Step 632: Agent task status overlay (12 tests) + +#include "AgentTaskStatusOverlay.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_summary_counts_running_and_pending() { + TEST(summary_counts_running_and_pending); + AgentTaskSlotsState state; + (void)AgentTaskSlots::setMaxSlots(&state, 2); + (void)AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::enqueueTask(&state, "b"); + (void)AgentTaskSlots::enqueueTask(&state, "c"); + (void)AgentTaskSlots::assignPendingToSlots(&state); + auto summary = AgentTaskStatusOverlay::buildSummary(state); + CHECK(summary.running == 2, "running count mismatch"); + CHECK(summary.pending == 1, "pending count mismatch"); + PASS(); +} + +void test_summary_label_format_matches_spec() { + TEST(summary_label_format_matches_spec); + AgentTaskSlotsState state; + (void)AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::assignPendingToSlots(&state); + auto summary = AgentTaskStatusOverlay::buildSummary(state); + CHECK(summary.label == "[Agent: 1 running, 0 pending]", "summary label mismatch"); + PASS(); +} + +void test_summary_has_work_false_when_no_tasks() { + TEST(summary_has_work_false_when_no_tasks); + AgentTaskSlotsState state; + auto summary = AgentTaskStatusOverlay::buildSummary(state); + CHECK(!summary.hasWork, "hasWork should be false"); + PASS(); +} + +void test_build_rows_returns_all_tasks() { + TEST(build_rows_returns_all_tasks); + AgentTaskSlotsState state; + (void)AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::enqueueTask(&state, "b"); + auto rows = AgentTaskStatusOverlay::buildRows(state, 0); + CHECK(rows.size() == 2, "rows size mismatch"); + PASS(); +} + +void test_running_row_elapsed_uses_now_minus_started() { + TEST(running_row_elapsed_uses_now_minus_started); + AgentTaskSlotsState state; + std::string id = AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::assignPendingToSlotsAt(&state, 100); + CHECK(AgentTaskSlots::tickTaskElapsed(&state, id, 109), "tick should succeed"); + auto rows = AgentTaskStatusOverlay::buildRows(state, 112); + CHECK(rows[0].elapsedSeconds == 12, "elapsed should use running now"); + PASS(); +} + +void test_non_running_row_uses_last_elapsed() { + TEST(non_running_row_uses_last_elapsed); + AgentTaskSlotsState state; + std::string id = AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::assignPendingToSlotsAt(&state, 100); + (void)AgentTaskSlots::tickTaskElapsed(&state, id, 115); + (void)AgentTaskSlots::completeTask(&state, id); + auto rows = AgentTaskStatusOverlay::buildRows(state, 220); + CHECK(rows[0].elapsedSeconds == 15, "completed row should keep last elapsed"); + PASS(); +} + +void test_row_can_cancel_for_pending_task() { + TEST(row_can_cancel_for_pending_task); + AgentTaskSlotsState state; + (void)AgentTaskSlots::setMaxSlots(&state, 1); + (void)AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::enqueueTask(&state, "b"); + (void)AgentTaskSlots::assignPendingToSlots(&state); + auto rows = AgentTaskStatusOverlay::buildRows(state, 0); + CHECK(rows[1].status == "pending", "second row should be pending"); + CHECK(rows[1].canCancel, "pending task should be cancellable"); + PASS(); +} + +void test_row_can_cancel_for_running_task() { + TEST(row_can_cancel_for_running_task); + AgentTaskSlotsState state; + (void)AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::assignPendingToSlots(&state); + auto rows = AgentTaskStatusOverlay::buildRows(state, 0); + CHECK(rows[0].status == "running", "row should be running"); + CHECK(rows[0].canCancel, "running task should be cancellable"); + PASS(); +} + +void test_row_cannot_cancel_for_completed_task() { + TEST(row_cannot_cancel_for_completed_task); + AgentTaskSlotsState state; + std::string id = AgentTaskSlots::enqueueTask(&state, "a"); + (void)AgentTaskSlots::assignPendingToSlots(&state); + (void)AgentTaskSlots::completeTask(&state, id); + auto rows = AgentTaskStatusOverlay::buildRows(state, 0); + CHECK(rows[0].status == "completed", "row should be completed"); + CHECK(!rows[0].canCancel, "completed task should not be cancellable"); + PASS(); +} + +void test_build_rows_maps_tool_calls_and_current_step() { + TEST(build_rows_maps_tool_calls_and_current_step); + AgentTaskSlotsState state; + std::string id = AgentTaskSlots::enqueueTask(&state, "a"); + CHECK(AgentTaskSlots::updateTaskProgress(&state, id, 3, "mutating AST"), "progress update failed"); + auto rows = AgentTaskStatusOverlay::buildRows(state, 0); + CHECK(rows[0].toolCallsMade == 3, "tool call count mismatch"); + CHECK(rows[0].currentStep == "mutating AST", "current step mismatch"); + PASS(); +} + +void test_build_rows_defaults_current_step_to_dash() { + TEST(build_rows_defaults_current_step_to_dash); + AgentTaskSlotsState state; + (void)AgentTaskSlots::enqueueTask(&state, "a"); + auto rows = AgentTaskStatusOverlay::buildRows(state, 0); + CHECK(rows[0].currentStep == "-", "default current step should be dash"); + PASS(); +} + +void test_cancel_task_marks_status_cancelled() { + TEST(cancel_task_marks_status_cancelled); + AgentTaskSlotsState state; + std::string id = AgentTaskSlots::enqueueTask(&state, "a"); + CHECK(AgentTaskStatusOverlay::cancelTask(&state, id), "cancel should succeed"); + auto rows = AgentTaskStatusOverlay::buildRows(state, 0); + CHECK(rows[0].status == "cancelled", "status should be cancelled"); + PASS(); +} + +int main() { + std::cout << "Step 632: Agent task status overlay\n"; + + test_summary_counts_running_and_pending(); // 1 + test_summary_label_format_matches_spec(); // 2 + test_summary_has_work_false_when_no_tasks(); // 3 + test_build_rows_returns_all_tasks(); // 4 + test_running_row_elapsed_uses_now_minus_started(); // 5 + test_non_running_row_uses_last_elapsed(); // 6 + test_row_can_cancel_for_pending_task(); // 7 + test_row_can_cancel_for_running_task(); // 8 + test_row_cannot_cancel_for_completed_task(); // 9 + test_build_rows_maps_tool_calls_and_current_step(); // 10 + test_build_rows_defaults_current_step_to_dash(); // 11 + test_cancel_task_marks_status_cancelled(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 085318a..9798203 100644 --- a/progress.md +++ b/progress.md @@ -12700,3 +12700,49 @@ assignment for concurrent chat threads. - `editor/src/state/AgentState.h` (`31` <= `600`) - `editor/tests/step631_test.cpp` within test-file size guidance (`161` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 632: Agent task status overlay +**Status:** PASS (12/12 tests) + +Adds agent task status visibility in the editor shell: +status bar summary (`[Agent: X running, Y pending]`) and detailed task rows in +the agent chat panel with description, elapsed time, tool-call count, current +step, and per-task cancel action. + +**Files added:** +- `editor/src/AgentTaskStatusOverlay.h` - overlay model: + - status summary composition for status bar rendering + - per-task row projection for task-panel rendering + - status text mapping and cancel dispatch helper +- `editor/tests/step632_test.cpp` - 12 tests covering: + - running/pending summary counts and label formatting + - empty-task edge behavior + - task-row projection, elapsed-time computation, and cancelability flags + - tool-call/current-step projection and cancellation semantics + +**Files modified:** +- `editor/src/AgentTaskSlots.h` - extended task metadata + helpers: + - start-time-aware slot assignment overload + - progress updates (`toolCallsMade`, `currentStep`) + - elapsed tracking for overlay reporting +- `editor/src/panels/StatusBarPanel.h` - status-bar agent item: + - renders overlay summary when work is active + - click opens the agent chat panel +- `editor/src/panels/AgentChatPanel.h` - task tabs + task list rendering: + - slot tabs for parallel task threads + - detailed task rows with cancel controls +- `editor/CMakeLists.txt` - `step632_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step632_test step631_test` - PASS +- `./editor/build-native/step632_test` - PASS (12/12) +- `./editor/build-native/step631_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/AgentTaskStatusOverlay.h` (`83` <= `600`) +- `editor/src/AgentTaskSlots.h` (`157` <= `600`) +- `editor/src/panels/StatusBarPanel.h` (`223` <= `600`) +- `editor/src/panels/AgentChatPanel.h` (`208` <= `600`) +- `editor/tests/step632_test.cpp` within test-file size guidance (`193` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`