Step 185: contextual feature hints

This commit is contained in:
Bill
2026-02-09 22:53:00 -07:00
parent d391d2feeb
commit db2c5d9b07
11 changed files with 156 additions and 1 deletions

View File

@@ -532,4 +532,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
| 2026-02-10 | Codex | Step 182: Problems panel overhaul with grouping, sortable table, and diagnostic badges on tabs. 2/2 tests pass (step182_test, step182_integration_test). |
| 2026-02-10 | Codex | Step 183: Tab reordering and untitled rename flow, plus buffer rename support. 2/2 tests pass (step183_test, step183_integration_test). |
| 2026-02-10 | Codex | Step 184: First-run wizard (layout/theme/keybindings + get-started actions) wired on missing session. 2/2 tests pass (step184_test, step184_integration_test). |
| 2026-02-10 | Codex | Step 185: Contextual feature hints with persistence + dismiss/disable. 2/2 tests pass (step185_test, step185_integration_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

@@ -1079,6 +1079,13 @@ add_executable(step184_integration_test tests/step184_integration_test.cpp)
target_include_directories(step184_integration_test PRIVATE src)
target_link_libraries(step184_integration_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step185_test tests/step185_test.cpp)
target_include_directories(step185_test PRIVATE src)
add_executable(step185_integration_test tests/step185_integration_test.cpp)
target_include_directories(step185_integration_test PRIVATE src)
target_link_libraries(step185_integration_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -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
View 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();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
// Step 185: Feature hints integration checks.
#include <cassert>
#include "FeatureHints.h"
int main() {
FeatureHintsState state;
queueFeatureHint(state, "hint.test", "Test hint");
assert(!state.activeMessage.empty());
printf("step185_integration_test: all assertions passed\n");
return 0;
}

View File

@@ -0,0 +1,25 @@
// Step 185: Feature hints checks.
#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 hints = readFile("src/FeatureHints.h");
assertContains(hints, "FeatureHintsState");
assertContains(hints, "renderFeatureHintBar");
printf("step185_test: all assertions passed\n");
return 0;
}

View File

@@ -249,7 +249,7 @@ without being annoying.
Settings persisted immediately. Can be re-run from Help > Setup Wizard.
*New:* `FirstRunWizard.h`
- [ ] **Step 185: Contextual feature hints**
- [x] **Step 185: Contextual feature hints**
Non-intrusive discovery system:
- Hints appear as subtle bar below toolbar, not modal dialogs
- Triggered by context: first time opening annotations panel →