Step 194: semantic-filtered library browser
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"}};
|
||||
|
||||
@@ -13,6 +13,7 @@ struct LibraryBrowserState {
|
||||
std::string selectedLibrary;
|
||||
std::string selectedSymbol;
|
||||
std::string docText;
|
||||
std::vector<std::string> 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<std::string>& 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<std::string> combinedTags(const ExternalModule* ext,
|
||||
const TypeSignature* sig) {
|
||||
std::vector<std::string> out = ext ? ext->semanticTags : std::vector<std::string>();
|
||||
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<std::string>& tags,
|
||||
const std::vector<std::string>& 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<std::string> collectAvailableTags(Module* ast) {
|
||||
std::vector<std::string> tags;
|
||||
if (!ast) return tags;
|
||||
for (auto* node : ast->getChildren("externalModules")) {
|
||||
auto* ext = static_cast<ExternalModule*>(node);
|
||||
for (const auto& tag : ext->semanticTags) tags.push_back(tag);
|
||||
for (auto* sigNode : ext->getChildren("signatures")) {
|
||||
auto* sig = static_cast<TypeSignature*>(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<TypeSignature*>(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<ExternalModule*>(node);
|
||||
if (ext->name != state.selectedLibrary) continue;
|
||||
for (auto* sigNode : ext->getChildren("signatures")) {
|
||||
auto* sig = static_cast<TypeSignature*>(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")) {
|
||||
|
||||
@@ -15,6 +15,7 @@ struct PrimitiveSymbol {
|
||||
std::string source; // builtin, import, user
|
||||
std::string library;
|
||||
bool vulnerable = false;
|
||||
std::vector<std::string> tags;
|
||||
};
|
||||
|
||||
class PrimitivesRegistry {
|
||||
@@ -24,6 +25,7 @@ public:
|
||||
void setVulnerableLibraries(const std::unordered_set<std::string>& libs) {
|
||||
vulnerableLibraries_ = libs;
|
||||
}
|
||||
void setContextTags(const std::vector<std::string>& tags) { contextTags_ = tags; }
|
||||
void setBlockVulnerableImports(bool block) { blockVulnerableImports_ = block; }
|
||||
|
||||
std::vector<PrimitiveSymbol> 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<std::string> vulnerableLibraries_;
|
||||
std::vector<std::string> contextTags_;
|
||||
bool blockVulnerableImports_ = false;
|
||||
};
|
||||
|
||||
@@ -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<std::string, std::unordered_map<std::string, TagList>> tags_;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../CompletionUtils.h"
|
||||
#include "../AnimationUtils.h"
|
||||
#include "../RichTooltip.h"
|
||||
#include "../ast/Function.h"
|
||||
#include <map>
|
||||
|
||||
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<std::string> 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<Function*>(cursorNode);
|
||||
contextTags = state.library.semanticTags.inferTagsFromText(fn->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.library.primitives.setContextTags(contextTags);
|
||||
std::vector<PrimitiveSymbol> primitives;
|
||||
auto funcs = state.library.primitives.getAvailableFunctions(nodeId);
|
||||
auto types = state.library.primitives.getAvailableTypes(nodeId);
|
||||
|
||||
29
editor/tests/step194_test.cpp
Normal file
29
editor/tests/step194_test.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
// Step 194: Semantic-filtered library browser.
|
||||
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user