Step 169: notification system

This commit is contained in:
Bill
2026-02-09 21:26:10 -07:00
parent 08b7d71a38
commit 454bd5eb8a
22 changed files with 637 additions and 167 deletions

View File

@@ -13,8 +13,30 @@ static void renderBottomPanel(EditorState& state) {
if (ImGui::BeginTabItem("Output")) {
ImGui::PushFont(state.monoFont);
ImGui::BeginChild("##outputScroll", ImVec2(0, 0), false);
ImGui::TextUnformatted(state.outputLog.c_str());
// Auto-scroll to bottom
for (const auto& note : state.notifications.getHistory()) {
const char* label = "Info";
ImVec4 color = ImVec4(0.40f, 0.70f, 0.95f, 1.0f);
switch (note.level) {
case NotificationLevel::Success:
label = "Success";
color = ImVec4(0.25f, 0.78f, 0.35f, 1.0f);
break;
case NotificationLevel::Warning:
label = "Warning";
color = ImVec4(0.95f, 0.72f, 0.28f, 1.0f);
break;
case NotificationLevel::Error:
label = "Error";
color = ImVec4(0.90f, 0.35f, 0.35f, 1.0f);
break;
case NotificationLevel::Info:
default:
break;
}
ImGui::TextColored(color, "[%s]", label);
ImGui::SameLine();
ImGui::TextWrapped("%s", note.message.c_str());
}
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20)
ImGui::SetScrollHereY(1.0f);
ImGui::EndChild();
@@ -86,7 +108,7 @@ static void renderBottomPanel(EditorState& state) {
ImGui::PushFont(state.monoFont);
renderAgentMarketplace(state.agent.marketplace,
state.agent.registry,
state.outputLog,
state.notifications,
state.workspaceRoot);
ImGui::PopFont();
ImGui::EndTabItem();
@@ -261,10 +283,10 @@ static void renderBottomPanel(EditorState& state) {
}
if (!warning.empty()) {
summary += " — warning: " + warning;
state.outputLog += warning + "\n";
state.notify(NotificationLevel::Warning, warning);
}
state.optimizeFoldSummary = summary;
state.outputLog += summary + "\n";
state.notify(NotificationLevel::Info, summary);
if (nodes > 0) state.applyOrchestratorToActive();
std::string afterText = state.active()->editBuf;
state.openDiff(beforeText, afterText, false, 1, {tid});
@@ -308,7 +330,7 @@ static void renderBottomPanel(EditorState& state) {
summary += "no changes";
}
state.optimizeDeadCodeSummary = summary;
state.outputLog += summary + "\n";
state.notify(NotificationLevel::Info, summary);
if (nodes > 0) state.applyOrchestratorToActive();
std::string afterText = state.active()->editBuf;
state.openDiff(beforeText, afterText, false, 2, {tid});
@@ -354,10 +376,10 @@ static void renderBottomPanel(EditorState& state) {
}
if (!warning.empty()) {
summary += " — warning: " + warning;
state.outputLog += warning + "\n";
state.notify(NotificationLevel::Warning, warning);
}
state.optimizeApplySummary = summary;
state.outputLog += summary + "\n";
state.notify(NotificationLevel::Info, summary);
if (totalNodes > 0) state.applyOrchestratorToActive();
std::string afterText = state.active()->editBuf;
state.openDiff(beforeText, afterText, false, 3, {tidFold, tidDce});
@@ -460,10 +482,11 @@ static void renderBottomPanel(EditorState& state) {
batch.setRoot(ast);
auto res = batch.applySequence(state.diff.batchMutations);
if (!res.success) {
state.outputLog += res.error + "\n";
state.notify(NotificationLevel::Error, res.error);
} else {
state.outputLog += "Refactor applied (" +
std::to_string(res.appliedCount) + " mutations).\n";
state.notify(NotificationLevel::Success,
"Refactor applied (" +
std::to_string(res.appliedCount) + " mutations).");
state.applyOrchestratorToActive();
}
} else {

View File

@@ -155,8 +155,9 @@ static void renderRefactorPopup(EditorState& state) {
if (!res.success) {
state.refactorError = res.error;
} else {
state.outputLog += "Refactor applied (" +
std::to_string(res.appliedCount) + " mutations).\n";
state.notify(NotificationLevel::Success,
"Refactor applied (" +
std::to_string(res.appliedCount) + " mutations).");
state.applyOrchestratorToActive();
state.showRefactorPopup = false;
}

View File

@@ -452,7 +452,7 @@ static void renderEditorPanel(EditorState& state) {
}
state.mutator.setRoot(ast);
auto res2 = state.mutator.insertNode(target->id, "annotations", anno);
if (!res2.error.empty()) state.outputLog += res2.error + "\n";
if (!res2.error.empty()) state.notify(NotificationLevel::Error, res2.error);
state.applyOrchestratorToActive();
}
}
@@ -555,7 +555,7 @@ static void renderEditorPanel(EditorState& state) {
target->spanEndLine, target->spanEndCol);
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}
@@ -566,7 +566,7 @@ static void renderEditorPanel(EditorState& state) {
target->spanEndLine, target->spanEndCol);
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}
@@ -577,7 +577,7 @@ static void renderEditorPanel(EditorState& state) {
target->spanEndLine, target->spanEndCol);
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}
@@ -588,7 +588,7 @@ static void renderEditorPanel(EditorState& state) {
target->spanEndLine, target->spanEndCol);
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}
@@ -599,7 +599,7 @@ static void renderEditorPanel(EditorState& state) {
target->spanEndLine, target->spanEndCol);
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}
@@ -615,35 +615,35 @@ static void renderEditorPanel(EditorState& state) {
if (ImGui::MenuItem("Tracing")) {
state.mutator.setRoot(ast);
auto res = state.mutator.setProperty(anno->id, "strategy", "Tracing");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "OwnerAnnotation") {
if (ImGui::MenuItem("Single")) {
state.mutator.setRoot(ast);
auto res = state.mutator.setProperty(anno->id, "strategy", "Single");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "DeallocateAnnotation") {
if (ImGui::MenuItem("Explicit")) {
state.mutator.setRoot(ast);
auto res = state.mutator.setProperty(anno->id, "strategy", "Explicit");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "LifetimeAnnotation") {
if (ImGui::MenuItem("RAII")) {
state.mutator.setRoot(ast);
auto res = state.mutator.setProperty(anno->id, "strategy", "RAII");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "AllocateAnnotation") {
if (ImGui::MenuItem("Static")) {
state.mutator.setRoot(ast);
auto res = state.mutator.setProperty(anno->id, "strategy", "Static");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}
@@ -659,7 +659,7 @@ static void renderEditorPanel(EditorState& state) {
if (ImGui::MenuItem(label.c_str())) {
state.mutator.setRoot(ast);
auto res = state.mutator.deleteNode(anno->id);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}
@@ -682,7 +682,7 @@ static void renderEditorPanel(EditorState& state) {
if (ImGui::MenuItem("Remove child annotation")) {
state.mutator.setRoot(ast);
auto res = state.mutator.deleteNode(lineConflict.childAnnoId);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
if (ImGui::MenuItem("Match parent strategy")) {
@@ -690,7 +690,7 @@ static void renderEditorPanel(EditorState& state) {
auto res = state.mutator.setProperty(lineConflict.childAnnoId,
"strategy",
lineConflict.parentStrategy);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (!res.error.empty()) state.notify(NotificationLevel::Error, res.error);
if (res.error.empty()) mutated = true;
}
}

View File

@@ -24,7 +24,8 @@ static void renderMenuBar(EditorState& state) {
auto path = FileDialog::openFile({"Open Project", {"*.whetstone"}, state.lastDialogPath});
if (!path.empty()) {
if (!state.loadProject(path)) {
state.outputLog += "Failed to open project: " + path + "\n";
state.notify(NotificationLevel::Error,
"Failed to open project: " + path);
} else {
state.lastDialogPath = path;
}
@@ -59,7 +60,8 @@ static void renderMenuBar(EditorState& state) {
auto path = FileDialog::saveFile({"Save Project", {"*.whetstone"}, state.lastDialogPath});
if (!path.empty()) {
if (!state.saveProject(path)) {
state.outputLog += "Failed to save project: " + path + "\n";
state.notify(NotificationLevel::Error,
"Failed to save project: " + path);
} else {
state.lastDialogPath = path;
}

View File

@@ -80,9 +80,11 @@ static void renderSettingsPanel(EditorState& state) {
UpdateChecker checker;
auto info = checker.check(state.settings.getUpdateUrl());
if (info.available) {
state.outputLog += "[update] Available: " + info.version + " (" + info.url + ")\n";
state.notify(NotificationLevel::Info,
"[update] Available: " + info.version + " (" + info.url + ")");
} else {
state.outputLog += "[update] No update info (offline/stub).\n";
state.notify(NotificationLevel::Info,
"[update] No update info (offline/stub).");
}
}

View File

@@ -80,7 +80,7 @@ static void renderDependenciesPanel(EditorState& state) {
if (!state.library.showDependencyPanel) return;
ImGui::Begin("Dependencies", &state.library.showDependencyPanel);
ImGui::PushFont(state.uiFont);
renderDependencyPanel(state.library.dependencyPanel, state.workspaceRoot, state.outputLog);
renderDependencyPanel(state.library.dependencyPanel, state.workspaceRoot, state.notifications);
ImGui::PopFont();
ImGui::End();
if (state.library.dependencyPanel.needsIndex) {
@@ -100,7 +100,7 @@ static void renderLibrariesPanel(EditorState& state) {
state.library.libraryIndex,
insertText,
insertLibrary,
state.outputLog)) {
state.notifications)) {
state.ensureImportForSymbol(insertLibrary, insertText);
state.insertTextAtCursor(insertText);
}
@@ -123,7 +123,7 @@ static void renderCompositionPanel(EditorState& state) {
auto funcs = state.library.primitives.getAvailableFunctions(nodeId);
primitives.insert(primitives.end(), funcs.begin(), funcs.end());
std::string code;
if (renderCompositionPanel(state.library.compositionPanel, primitives, code, state.outputLog)) {
if (renderCompositionPanel(state.library.compositionPanel, primitives, code, state.notifications)) {
state.insertTextAtCursor(code);
}
ImGui::PopFont();
@@ -134,7 +134,9 @@ static void renderEmacsPackagesPanel(EditorState& state) {
if (!state.emacsState.showEmacsPackagesPanel) return;
ImGui::Begin("Emacs Packages", &state.emacsState.showEmacsPackagesPanel);
ImGui::PushFont(state.uiFont);
if (renderEmacsPackageBrowser(state.emacsState.emacsPackages, state.emacsState.emacs, state.outputLog)) {
if (renderEmacsPackageBrowser(state.emacsState.emacsPackages,
state.emacsState.emacs,
state.notifications)) {
state.emacsState.emacsFunctionIndexDirty = true;
}
ImGui::PopFont();
@@ -188,7 +190,9 @@ static void renderMinibuffer(EditorState& state) {
state.emacsState.emacsKeys.minibufferBuf,
sizeof(state.emacsState.emacsKeys.minibufferBuf),
ImGuiInputTextFlags_EnterReturnsTrue)) {
emacsExecuteMinibuffer(state.emacsState.emacsKeys, state.emacsState.emacs, state.outputLog);
emacsExecuteMinibuffer(state.emacsState.emacsKeys,
state.emacsState.emacs,
state.notifications);
}
ImGui::PopFont();
ImGui::End();
@@ -237,7 +241,8 @@ static void renderMemoryStrategiesPanel(EditorState& state) {
if (ImGui::Button("Export JSON")) {
auto j = buildAnnotationSummaryJson(entries);
state.outputLog += "Annotation summary:\\n" + j.dump(2) + "\\n";
state.notify(NotificationLevel::Info,
"Annotation summary:\\n" + j.dump(2));
}
}
ImGui::PopFont();

View File

@@ -68,6 +68,16 @@ static void renderStatusBar(EditorState& state) {
ImGui::SameLine(0, 30);
ImGui::Text("UTF-8");
ImGui::SameLine(0, 30);
int unread = state.notifications.unreadCount();
std::string notifLabel = "Notifications";
if (unread > 0) {
notifLabel += " (" + std::to_string(unread) + ")";
}
if (ImGui::Button(notifLabel.c_str())) {
state.notifications.showHistory = !state.notifications.showHistory;
}
ImGui::PopFont();
ImGui::End();
ImGui::PopStyleVar();