From db2c5d9b07ecbe84400d06ea5bc12e681e5a0bdd Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 22:53:00 -0700 Subject: [PATCH] Step 185: contextual feature hints --- PROGRESS.md | 1 + editor/CMakeLists.txt | 7 ++ editor/src/EditorState.h | 3 + editor/src/FeatureHints.h | 92 +++++++++++++++++++++++ editor/src/main.cpp | 2 + editor/src/panels/BottomPanel.h | 3 + editor/src/panels/EditorPanel.h | 5 ++ editor/src/panels/SidePanels.h | 3 + editor/tests/step185_integration_test.cpp | 14 ++++ editor/tests/step185_test.cpp | 25 ++++++ sprint6_plan.md | 2 +- 11 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 editor/src/FeatureHints.h create mode 100644 editor/tests/step185_integration_test.cpp create mode 100644 editor/tests/step185_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index 42c008e..011c61f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 468dec6..18e2bf0 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index a5b24d3..6afd275 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -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&) { diff --git a/editor/src/FeatureHints.h b/editor/src/FeatureHints.h new file mode 100644 index 0000000..1573280 --- /dev/null +++ b/editor/src/FeatureHints.h @@ -0,0 +1,92 @@ +#pragma once + +#include "imgui.h" +#include + +#include +#include +#include +#include + +struct FeatureHintsState { + bool disabled = false; + bool shownThisSession = false; + std::string activeKey; + std::string activeMessage; + std::map 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(); + } + } +} + +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(); +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index c6f32ca..d6f2707 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -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); }); diff --git a/editor/src/panels/BottomPanel.h b/editor/src/panels/BottomPanel.h index 9fe7aaa..7841cf9 100644 --- a/editor/src/panels/BottomPanel.h +++ b/editor/src/panels/BottomPanel.h @@ -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); diff --git a/editor/src/panels/EditorPanel.h b/editor/src/panels/EditorPanel.h index ead9000..19db219 100644 --- a/editor/src/panels/EditorPanel.h +++ b/editor/src/panels/EditorPanel.h @@ -8,6 +8,11 @@ #include 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 // --------------------------------------------------------------- diff --git a/editor/src/panels/SidePanels.h b/editor/src/panels/SidePanels.h index c8034ed..57948d6 100644 --- a/editor/src/panels/SidePanels.h +++ b/editor/src/panels/SidePanels.h @@ -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)); diff --git a/editor/tests/step185_integration_test.cpp b/editor/tests/step185_integration_test.cpp new file mode 100644 index 0000000..15f6a88 --- /dev/null +++ b/editor/tests/step185_integration_test.cpp @@ -0,0 +1,14 @@ +// Step 185: Feature hints integration checks. + +#include + +#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; +} diff --git a/editor/tests/step185_test.cpp b/editor/tests/step185_test.cpp new file mode 100644 index 0000000..76c6591 --- /dev/null +++ b/editor/tests/step185_test.cpp @@ -0,0 +1,25 @@ +// Step 185: Feature hints checks. + +#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 hints = readFile("src/FeatureHints.h"); + assertContains(hints, "FeatureHintsState"); + assertContains(hints, "renderFeatureHintBar"); + + printf("step185_test: all assertions passed\n"); + return 0; +} diff --git a/sprint6_plan.md b/sprint6_plan.md index dfe14d9..3d0b83b 100644 --- a/sprint6_plan.md +++ b/sprint6_plan.md @@ -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 →