diff --git a/PROGRESS.md b/PROGRESS.md index 8930fc5..44d0a90 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -541,4 +541,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step | 2026-02-10 | Codex | Step 191: Dependency security badges with severity colors, advisory details, safe upgrade path, and ignore list persistence. 1/1 tests pass (step191_test). | | 2026-02-10 | Codex | Step 192: Security diagnostics integration (gutter shields, [Security] problems, vulnerable import blocking, and agent import deprioritization). 1/1 tests pass (step192_test). | | 2026-02-10 | Codex | Step 193: Semantic library tags (semantic_tags.json storage, auto-tagging heuristics, tags attached to ExternalModule/TypeSignature). 1/1 tests pass (step193_test). | +| 2026-02-10 | Codex | Step 194: Semantic-filtered library browser (tag filter bar, active tag chips, tag badges, context-aware completion boosting, agent tag context). 1/1 tests pass (step194_test). | | 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index e26dd28..a257e20 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1119,6 +1119,9 @@ target_include_directories(step192_test PRIVATE src) add_executable(step193_test tests/step193_test.cpp) target_include_directories(step193_test PRIVATE src) +add_executable(step194_test tests/step194_test.cpp) +target_include_directories(step194_test PRIVATE src) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/AgentCodeGen.h b/editor/src/AgentCodeGen.h index bb9110f..d4b49ad 100644 --- a/editor/src/AgentCodeGen.h +++ b/editor/src/AgentCodeGen.h @@ -79,6 +79,13 @@ private: std::string note = "Generated using " + sym.name + " (" + sym.source + ")"; if (!spec.empty()) note += " for spec: " + spec; if (!language.empty()) note += " in " + language; + if (!sym.tags.empty()) { + note += " | tags: "; + for (size_t i = 0; i < sym.tags.size(); ++i) { + if (i > 0) note += ", "; + note += sym.tags[i]; + } + } return note; } diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 00ecc50..68f963b 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -1365,6 +1365,7 @@ struct EditorState { library.primitives.setLanguage(active()->language); AgentCodeGen gen; + library.primitives.setContextTags(library.semanticTags.inferTagsFromText(spec)); AgentCodeGenResult genRes = gen.generate(spec, library.primitives, active()->language, preferImports); if (!genRes.node) { response["error"] = {{"code", -32020}, {"message", "Code generation failed"}}; diff --git a/editor/src/LibraryBrowserPanel.h b/editor/src/LibraryBrowserPanel.h index c42df4b..c4f90b2 100644 --- a/editor/src/LibraryBrowserPanel.h +++ b/editor/src/LibraryBrowserPanel.h @@ -13,6 +13,7 @@ struct LibraryBrowserState { std::string selectedLibrary; std::string selectedSymbol; std::string docText; + std::vector activeTags; }; static std::string formatUsageTemplate(const std::string& symbol) { @@ -54,6 +55,51 @@ static bool symbolMatchesFilter(const std::string& symbol, const std::string& fi return a.find(b) != std::string::npos; } +static std::string joinTags(const std::vector& tags) { + std::string out; + for (size_t i = 0; i < tags.size(); ++i) { + if (i > 0) out += " "; + out += tags[i]; + } + return out; +} + +static std::vector combinedTags(const ExternalModule* ext, + const TypeSignature* sig) { + std::vector out = ext ? ext->semanticTags : std::vector(); + if (sig) { + out.insert(out.end(), sig->semanticTags.begin(), sig->semanticTags.end()); + } + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + return out; +} + +static bool symbolMatchesTags(const std::vector& tags, + const std::vector& active) { + if (active.empty()) return true; + for (const auto& tag : active) { + if (std::find(tags.begin(), tags.end(), tag) == tags.end()) return false; + } + return true; +} + +static std::vector collectAvailableTags(Module* ast) { + std::vector tags; + if (!ast) return tags; + for (auto* node : ast->getChildren("externalModules")) { + auto* ext = static_cast(node); + for (const auto& tag : ext->semanticTags) tags.push_back(tag); + for (auto* sigNode : ext->getChildren("signatures")) { + auto* sig = static_cast(sigNode); + for (const auto& tag : sig->semanticTags) tags.push_back(tag); + } + } + std::sort(tags.begin(), tags.end()); + tags.erase(std::unique(tags.begin(), tags.end()), tags.end()); + return tags; +} + static bool renderLibraryBrowser(LibraryBrowserState& state, Module* ast, const LibraryIndexData& index, @@ -67,6 +113,46 @@ static bool renderLibraryBrowser(LibraryBrowserState& state, return false; } + auto availableTags = collectAvailableTags(ast); + if (!availableTags.empty()) { + if (!state.activeTags.empty()) { + ImGui::TextUnformatted("Active tags"); + for (const auto& tag : state.activeTags) { + ImGui::PushID(tag.c_str()); + ImGui::TextDisabled("%s", tag.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) { + state.activeTags.erase(std::remove(state.activeTags.begin(), + state.activeTags.end(), tag), + state.activeTags.end()); + ImGui::PopID(); + break; + } + ImGui::SameLine(); + ImGui::PopID(); + } + ImGui::NewLine(); + } + ImGui::TextUnformatted("Filter by tag"); + for (const auto& tag : availableTags) { + bool active = std::find(state.activeTags.begin(), state.activeTags.end(), tag) + != state.activeTags.end(); + if (active) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + if (ImGui::SmallButton(tag.c_str())) { + if (active) { + state.activeTags.erase(std::remove(state.activeTags.begin(), + state.activeTags.end(), tag), + state.activeTags.end()); + } else { + state.activeTags.push_back(tag); + } + } + if (active) ImGui::PopStyleColor(); + ImGui::SameLine(); + } + ImGui::NewLine(); + } + ImGui::InputText("Filter", state.filterBuf, sizeof(state.filterBuf)); std::string filter = state.filterBuf; @@ -86,7 +172,9 @@ static bool renderLibraryBrowser(LibraryBrowserState& state, auto sigs = ext->getChildren("signatures"); for (auto* sigNode : sigs) { auto* sig = static_cast(sigNode); + auto tags = combinedTags(ext, sig); if (!symbolMatchesFilter(sig->name, filter)) continue; + if (!symbolMatchesTags(tags, state.activeTags)) continue; std::string label = sig->name; bool selected = (state.selectedLibrary == ext->name && state.selectedSymbol == sig->name); if (ImGui::Selectable(label.c_str(), selected)) { @@ -94,6 +182,10 @@ static bool renderLibraryBrowser(LibraryBrowserState& state, state.selectedSymbol = sig->name; updateDocText(state, index, ext->name, sig->name); } + if (!tags.empty()) { + ImGui::SameLine(); + ImGui::TextDisabled("%s", joinTags(tags).c_str()); + } if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { std::string usage = formatUsageTemplate(sig->name); ImGui::SetDragDropPayload("WhetstoneSymbol", usage.c_str(), usage.size() + 1); @@ -108,6 +200,20 @@ static bool renderLibraryBrowser(LibraryBrowserState& state, if (!state.selectedSymbol.empty()) { ImGui::Separator(); ImGui::Text("Selected: %s", state.selectedSymbol.c_str()); + for (auto* node : modules) { + auto* ext = static_cast(node); + if (ext->name != state.selectedLibrary) continue; + for (auto* sigNode : ext->getChildren("signatures")) { + auto* sig = static_cast(sigNode); + if (sig->name != state.selectedSymbol) continue; + auto tags = combinedTags(ext, sig); + if (!tags.empty()) { + ImGui::TextDisabled("Tags: %s", joinTags(tags).c_str()); + } + break; + } + break; + } ImGui::TextWrapped("%s", state.docText.c_str()); std::string usage = formatUsageTemplate(state.selectedSymbol); if (ImGui::Button("Insert")) { diff --git a/editor/src/PrimitivesRegistry.h b/editor/src/PrimitivesRegistry.h index 768cd08..2ec46ee 100644 --- a/editor/src/PrimitivesRegistry.h +++ b/editor/src/PrimitivesRegistry.h @@ -15,6 +15,7 @@ struct PrimitiveSymbol { std::string source; // builtin, import, user std::string library; bool vulnerable = false; + std::vector tags; }; class PrimitivesRegistry { @@ -24,6 +25,7 @@ public: void setVulnerableLibraries(const std::unordered_set& libs) { vulnerableLibraries_ = libs; } + void setContextTags(const std::vector& tags) { contextTags_ = tags; } void setBlockVulnerableImports(bool block) { blockVulnerableImports_ = block; } std::vector getAvailableFunctions(const std::string& nodeId = "") { @@ -61,6 +63,10 @@ private: ps.source = "import"; ps.library = ext->name; ps.vulnerable = (vulnerableLibraries_.find(ps.library) != vulnerableLibraries_.end()); + ps.tags = sig->semanticTags; + ps.tags.insert(ps.tags.end(), + ext->semanticTags.begin(), + ext->semanticTags.end()); ps.kind = classifySymbol(ps.name); if (!filterKind.empty() && ps.kind != filterKind) continue; if (ps.vulnerable && blockVulnerableImports_) continue; @@ -86,13 +92,25 @@ private: } if (!out.empty()) { + auto score = [this](const PrimitiveSymbol& sym) { + int s = 0; + if (!contextTags_.empty()) { + for (const auto& tag : sym.tags) { + if (std::find(contextTags_.begin(), contextTags_.end(), tag) != contextTags_.end()) { + s += 5; + break; + } + } + } + if (sym.source == "import") s += 1; + if (sym.vulnerable) s -= 5; + return s; + }; std::stable_sort(out.begin(), out.end(), - [](const PrimitiveSymbol& a, const PrimitiveSymbol& b) { - if (a.source == "import" && b.source == "import") { - if (a.vulnerable != b.vulnerable) { - return !a.vulnerable && b.vulnerable; - } - } + [&](const PrimitiveSymbol& a, const PrimitiveSymbol& b) { + int sa = score(a); + int sb = score(b); + if (sa != sb) return sa > sb; return false; }); } @@ -151,5 +169,6 @@ private: Module* root_ = nullptr; std::string language_ = "python"; std::unordered_set vulnerableLibraries_; + std::vector contextTags_; bool blockVulnerableImports_ = false; }; diff --git a/editor/src/SemanticTags.h b/editor/src/SemanticTags.h index 897018c..8aade01 100644 --- a/editor/src/SemanticTags.h +++ b/editor/src/SemanticTags.h @@ -94,6 +94,10 @@ public: return dedupe(out); } + TagList inferTagsFromText(const std::string& text) const { + return dedupe(inferTagsFromName(text)); + } + private: bool loaded_ = false; std::unordered_map> tags_; diff --git a/editor/src/panels/EditorPanel.h b/editor/src/panels/EditorPanel.h index 0ddd97d..0b6b976 100644 --- a/editor/src/panels/EditorPanel.h +++ b/editor/src/panels/EditorPanel.h @@ -5,6 +5,7 @@ #include "../CompletionUtils.h" #include "../AnimationUtils.h" #include "../RichTooltip.h" +#include "../ast/Function.h" #include static void renderEditorPanel(EditorState& state) { @@ -397,12 +398,24 @@ static void renderEditorPanel(EditorState& state) { int cursor = state.active()->widget.getCursor(); std::string prefix = EditorState::wordPrefixAt(state.active()->editBuf, cursor); std::string nodeId; + std::vector contextTags; if (state.activeAST()) { ASTNode* scopeNode = findNodeAtPosition(state.activeAST(), std::max(0, state.active()->cursorLine - 1), std::max(0, state.active()->cursorCol - 1)); - if (scopeNode) nodeId = scopeNode->id; + if (scopeNode) { + nodeId = scopeNode->id; + ASTNode* cursorNode = scopeNode; + while (cursorNode && cursorNode->conceptType != "Function") { + cursorNode = cursorNode->parent; + } + if (cursorNode && cursorNode->conceptType == "Function") { + auto* fn = static_cast(cursorNode); + contextTags = state.library.semanticTags.inferTagsFromText(fn->name); + } + } } + state.library.primitives.setContextTags(contextTags); std::vector primitives; auto funcs = state.library.primitives.getAvailableFunctions(nodeId); auto types = state.library.primitives.getAvailableTypes(nodeId); diff --git a/editor/tests/step194_test.cpp b/editor/tests/step194_test.cpp new file mode 100644 index 0000000..10795a0 --- /dev/null +++ b/editor/tests/step194_test.cpp @@ -0,0 +1,29 @@ +// Step 194: Semantic-filtered library browser. + +#include +#include +#include + +static std::string readFile(const std::string& path) { + std::ifstream f(path); + if (!f.is_open()) return {}; + return std::string((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); +} + +static void assertContains(const std::string& text, const std::string& needle) { + assert(text.find(needle) != std::string::npos); +} + +int main() { + const std::string browser = readFile("src/LibraryBrowserPanel.h"); + const std::string registry = readFile("src/PrimitivesRegistry.h"); + + assertContains(browser, "Filter by tag"); + assertContains(browser, "Active tags"); + assertContains(browser, "semanticTags"); + assertContains(registry, "setContextTags"); + + printf("step194_test: all assertions passed\n"); + return 0; +} diff --git a/sprint6_plan.md b/sprint6_plan.md index 83c5227..fe315b8 100644 --- a/sprint6_plan.md +++ b/sprint6_plan.md @@ -356,7 +356,7 @@ feature requests with polished UX. - Tags attached to `ExternalModule` / `TypeSignature` AST nodes *New:* `SemanticTags.h` -- [ ] **Step 194: Semantic-filtered library browser** +- [x] **Step 194: Semantic-filtered library browser** Enhance Library Browser with tag-based discovery: - Tag filter bar above symbol list: click tags to filter - Active tags shown as chips, click X to remove