Step 185: contextual feature hints
This commit is contained in:
@@ -52,6 +52,7 @@
|
||||
#include "NotificationSystem.h"
|
||||
#include "UIEventBus.h"
|
||||
#include "SearchUtils.h"
|
||||
#include "FeatureHints.h"
|
||||
#include "state/SearchState.h"
|
||||
#include "state/AgentState.h"
|
||||
#include "state/BuildState.h"
|
||||
@@ -182,6 +183,7 @@ struct EditorState {
|
||||
UIEventBus events;
|
||||
HelpPanelState helpPanel;
|
||||
Telemetry telemetry;
|
||||
FeatureHintsState featureHints;
|
||||
char outlineFilter[128] = {};
|
||||
WelcomeScreen welcome;
|
||||
std::string workspaceRoot;
|
||||
@@ -803,6 +805,7 @@ struct EditorState {
|
||||
search.projectSearch.setRoot(workspaceRoot);
|
||||
fileTreeDirty = true;
|
||||
loadRecentFiles();
|
||||
loadFeatureHints(featureHints, workspaceRoot);
|
||||
loadSettingsFromDisk();
|
||||
registerCommands();
|
||||
events.subscribe(UIEventType::BufferSwitched, [this](const UIEvent&) {
|
||||
|
||||
92
editor/src/FeatureHints.h
Normal file
92
editor/src/FeatureHints.h
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
struct FeatureHintsState {
|
||||
bool disabled = false;
|
||||
bool shownThisSession = false;
|
||||
std::string activeKey;
|
||||
std::string activeMessage;
|
||||
std::map<std::string, int> shownCounts;
|
||||
};
|
||||
|
||||
static std::filesystem::path hintsFilePath(const std::string& workspaceRoot) {
|
||||
const char* home = std::getenv("USERPROFILE");
|
||||
if (!home) home = std::getenv("HOME");
|
||||
std::filesystem::path base = home ? std::filesystem::path(home) : std::filesystem::current_path();
|
||||
(void)workspaceRoot;
|
||||
return base / ".whetstone" / "hints.json";
|
||||
}
|
||||
|
||||
static void loadFeatureHints(FeatureHintsState& state, const std::string& workspaceRoot) {
|
||||
std::filesystem::path path = hintsFilePath(workspaceRoot);
|
||||
if (!std::filesystem::exists(path)) return;
|
||||
std::ifstream in(path.string());
|
||||
if (!in.is_open()) return;
|
||||
nlohmann::json j;
|
||||
in >> j;
|
||||
state.disabled = j.value("disabled", false);
|
||||
if (j.contains("shown") && j["shown"].is_object()) {
|
||||
for (auto it = j["shown"].begin(); it != j["shown"].end(); ++it) {
|
||||
state.shownCounts[it.key()] = it.value().get<int>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void saveFeatureHints(const FeatureHintsState& state, const std::string& workspaceRoot) {
|
||||
std::filesystem::path path = hintsFilePath(workspaceRoot);
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
nlohmann::json j;
|
||||
j["disabled"] = state.disabled;
|
||||
j["shown"] = nlohmann::json::object();
|
||||
for (const auto& [key, count] : state.shownCounts) {
|
||||
j["shown"][key] = count;
|
||||
}
|
||||
std::ofstream out(path.string(), std::ios::binary);
|
||||
if (!out.is_open()) return;
|
||||
out << j.dump(2);
|
||||
}
|
||||
|
||||
static void queueFeatureHint(FeatureHintsState& state,
|
||||
const std::string& key,
|
||||
const std::string& message) {
|
||||
if (state.disabled || state.shownThisSession) return;
|
||||
if (state.shownCounts[key] > 0) return;
|
||||
state.activeKey = key;
|
||||
state.activeMessage = message;
|
||||
}
|
||||
|
||||
static void renderFeatureHintBar(FeatureHintsState& state, const std::string& workspaceRoot) {
|
||||
if (state.activeMessage.empty()) return;
|
||||
|
||||
ImGui::SetNextWindowBgAlpha(0.95f);
|
||||
ImGui::Begin("##FeatureHintBar", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoDocking |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::TextUnformatted(state.activeMessage.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Got it")) {
|
||||
state.shownCounts[state.activeKey] = 1;
|
||||
state.shownThisSession = true;
|
||||
state.activeKey.clear();
|
||||
state.activeMessage.clear();
|
||||
saveFeatureHints(state, workspaceRoot);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Don't show tips")) {
|
||||
state.disabled = true;
|
||||
state.shownThisSession = true;
|
||||
state.activeKey.clear();
|
||||
state.activeMessage.clear();
|
||||
saveFeatureHints(state, workspaceRoot);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "CompletionUtils.h"
|
||||
#include "ThemeEngine.h"
|
||||
#include "FirstRunWizard.h"
|
||||
#include "FeatureHints.h"
|
||||
|
||||
// --- Panel headers ---
|
||||
#include "panels/MenuBarPanel.h"
|
||||
@@ -378,6 +379,7 @@ int main(int, char**) {
|
||||
wizard.open = state.ui.showFirstRunWizard;
|
||||
renderFirstRunWizard(state, wizard);
|
||||
state.ui.showFirstRunWizard = wizard.open;
|
||||
renderFeatureHintBar(state.featureHints, state.workspaceRoot);
|
||||
state.notifications.renderHistoryWindow([&](const Notification& note) {
|
||||
if (note.hasTarget) state.navigateToTarget(note.target);
|
||||
});
|
||||
|
||||
@@ -100,6 +100,9 @@ static void renderBottomPanel(EditorState& state) {
|
||||
|
||||
if (ImGui::BeginTabItem("Agents")) {
|
||||
ImGui::PushFont(state.monoFont);
|
||||
queueFeatureHint(state.featureHints,
|
||||
"hint.agents",
|
||||
"Tip: Agents can query and modify your AST via the JSON-RPC API.");
|
||||
bool running = state.agent.server && state.agent.server->isRunning();
|
||||
ImGui::Text("Server: %s", running ? "Running" : "Stopped");
|
||||
ImGui::SameLine(0, 20);
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
#include <map>
|
||||
|
||||
static void renderEditorPanel(EditorState& state) {
|
||||
if (state.ui.showAnnotations) {
|
||||
queueFeatureHint(state.featureHints,
|
||||
"hint.annotations",
|
||||
"Tip: Right-click a function to add memory annotations.");
|
||||
}
|
||||
// ---------------------------------------------------------------
|
||||
// Editor (center) -- editable text area
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
static void renderOutlinePanel(EditorState& state) {
|
||||
if (!state.ui.showOutline) return;
|
||||
ImGui::Begin("Outline", &state.ui.showOutline);
|
||||
queueFeatureHint(state.featureHints,
|
||||
"hint.outline",
|
||||
"Tip: Use the Outline to jump between symbols quickly.");
|
||||
ImGui::PushFont(state.uiFont);
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
ImGui::InputText("Filter", state.outlineFilter, sizeof(state.outlineFilter));
|
||||
|
||||
Reference in New Issue
Block a user