Step 194: semantic-filtered library browser

This commit is contained in:
Bill
2026-02-10 02:18:25 -07:00
parent 331999ce14
commit 838082466b
10 changed files with 191 additions and 8 deletions

View File

@@ -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. |

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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"}};

View File

@@ -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")) {

View File

@@ -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;
};

View File

@@ -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_;

View File

@@ -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);

View 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;
}

View File

@@ -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