Step 141: Emacs daemon config
This commit is contained in:
@@ -382,7 +382,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
|
||||
## What's Next
|
||||
|
||||
Sprint 5 in progress. Step 140 (constructive coding tests) done. Next: Step 141 (Emacs daemon with user config).
|
||||
Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step 142 (Elisp package browser).
|
||||
|
||||
---
|
||||
|
||||
@@ -486,5 +486,6 @@ Sprint 5 in progress. Step 140 (constructive coding tests) done. Next: Step 141
|
||||
| 2026-02-09 | Codex | Step 138: Type-aware C++ generation via library call mappings. 2/2 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 139: Library compatibility matrix with default mappings. 3/3 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 140: Constructive coding integration tests covering imports, policy modes, composition, and type-aware generation. 7/7 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 141: Emacs daemon startup uses user config path with init logging and diagnostics. 4/4 tests pass. |
|
||||
| 2026-02-09 | Codex | Added FEATURE_REQUESTS.md to track security vulnerability awareness and semantic library annotations for future sprints. |
|
||||
| 2026-02-09 | Codex | Added LLM tooling + MCP bridge feature request to FEATURE_REQUESTS.md. |
|
||||
|
||||
@@ -778,6 +778,10 @@ add_executable(step140_test tests/step140_test.cpp)
|
||||
target_include_directories(step140_test PRIVATE src)
|
||||
target_link_libraries(step140_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step141_test tests/step141_test.cpp)
|
||||
target_include_directories(step141_test PRIVATE src)
|
||||
target_link_libraries(step141_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "imgui.h"
|
||||
#include "CompositionBuilder.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
|
||||
struct CompositionPanelState {
|
||||
@@ -10,6 +11,7 @@ struct CompositionPanelState {
|
||||
char functionName[64] = "compose_pipeline";
|
||||
std::string inputVar = "x";
|
||||
int selected = -1;
|
||||
std::vector<CompositionStep> steps;
|
||||
};
|
||||
|
||||
static bool renderCompositionPanel(CompositionPanelState& state,
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "AgentLibraryPolicy.h"
|
||||
#include "CompositionPanel.h"
|
||||
#include "IncrementalOptimizer.h"
|
||||
#include "EmacsIntegration.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Generator.h"
|
||||
#include "ast/Annotation.h"
|
||||
@@ -72,6 +73,7 @@
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sstream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -136,6 +138,7 @@ struct EditorState {
|
||||
BufferManager buffers;
|
||||
std::map<std::string, std::unique_ptr<BufferState>> bufferStates;
|
||||
BufferState* activeBuffer = nullptr;
|
||||
EmacsConnection emacs;
|
||||
|
||||
// Find/Replace state
|
||||
bool showFind = false;
|
||||
@@ -224,6 +227,7 @@ struct EditorState {
|
||||
bool analysisPending = false;
|
||||
double analysisLastChange = 0.0;
|
||||
std::vector<EditorDiagnostic> whetstoneDiagnostics;
|
||||
std::vector<EditorDiagnostic> emacsDiagnostics;
|
||||
SettingsManager settings;
|
||||
bool showLspSettings = false;
|
||||
bool showSettingsPanel = false;
|
||||
@@ -367,11 +371,11 @@ struct EditorState {
|
||||
|
||||
void insertTextAtCursor(const std::string& text) {
|
||||
if (!active() || text.empty()) return;
|
||||
bool changed = false;
|
||||
active()->widget.insertText(active()->editBuf, text, changed);
|
||||
if (changed) {
|
||||
onTextChanged();
|
||||
}
|
||||
int cursor = active()->widget.getCursor();
|
||||
cursor = std::max(0, std::min(cursor, (int)active()->editBuf.size()));
|
||||
active()->editBuf.insert(cursor, text);
|
||||
active()->widget.setCursor(cursor + (int)text.size());
|
||||
onTextChanged();
|
||||
}
|
||||
|
||||
void ensureImportForSymbol(const std::string& library, const std::string& symbol) {
|
||||
@@ -779,10 +783,55 @@ struct EditorState {
|
||||
loadRecentFiles();
|
||||
loadSettingsFromDisk();
|
||||
registerCommands();
|
||||
startEmacsDaemonFromSettings();
|
||||
initAgentServer();
|
||||
refreshBuildSystem();
|
||||
}
|
||||
|
||||
void startEmacsDaemonFromSettings() {
|
||||
emacsDiagnostics.clear();
|
||||
std::string initPath = resolveEmacsInitPath();
|
||||
std::string log;
|
||||
bool ok = emacs.startDaemonWithConfig(initPath, log);
|
||||
if (!log.empty()) {
|
||||
outputLog += "[emacs] " + log + "\n";
|
||||
}
|
||||
if (!ok) {
|
||||
EditorDiagnostic d;
|
||||
d.uri = "emacs://init";
|
||||
d.severity = 1;
|
||||
d.message = "[Emacs] " + emacs.getLastError();
|
||||
d.source = "EmacsDaemon";
|
||||
emacsDiagnostics.push_back(d);
|
||||
return;
|
||||
}
|
||||
if (!log.empty()) {
|
||||
addEmacsLogDiagnostics(log);
|
||||
}
|
||||
}
|
||||
|
||||
std::string resolveEmacsInitPath() const {
|
||||
return ::resolveEmacsInitPath(settings.getEmacsConfigPath());
|
||||
}
|
||||
|
||||
void addEmacsLogDiagnostics(const std::string& log) {
|
||||
std::istringstream ss(log);
|
||||
std::string line;
|
||||
while (std::getline(ss, line)) {
|
||||
std::string lower = line;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
if (lower.find("error") != std::string::npos) {
|
||||
EditorDiagnostic d;
|
||||
d.uri = "emacs://init";
|
||||
d.severity = 1;
|
||||
d.message = "[Emacs] " + line;
|
||||
d.source = "EmacsDaemon";
|
||||
emacsDiagnostics.push_back(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initAgentServer() {
|
||||
auto transport = std::make_unique<MockWebSocketTransport>();
|
||||
agentTransport = transport.get();
|
||||
@@ -952,8 +1001,20 @@ struct EditorState {
|
||||
});
|
||||
}
|
||||
|
||||
json emacsArr = json::array();
|
||||
for (const auto& d : emacsDiagnostics) {
|
||||
emacsArr.push_back({
|
||||
{"uri", d.uri},
|
||||
{"message", d.message},
|
||||
{"severity", d.severity},
|
||||
{"line", d.line},
|
||||
{"character", d.character}
|
||||
});
|
||||
}
|
||||
|
||||
out["lsp"] = lspArr;
|
||||
out["whetstone"] = whetArr;
|
||||
out["emacs"] = emacsArr;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ElispCommandBuilder — produces valid Elisp command strings
|
||||
@@ -79,19 +80,37 @@ public:
|
||||
|
||||
// Start Emacs daemon
|
||||
bool startDaemon() {
|
||||
std::string cmd = emacsPath_ + " --daemon 2>&1";
|
||||
int result = runCommand(cmd);
|
||||
std::string output;
|
||||
return startDaemonWithConfig("", output);
|
||||
}
|
||||
|
||||
// Start Emacs daemon with an explicit init file path
|
||||
virtual bool startDaemonWithConfig(const std::string& initFilePath, std::string& outLog) {
|
||||
outLog.clear();
|
||||
std::string cmd = emacsPath_ + " --daemon";
|
||||
if (!initFilePath.empty()) {
|
||||
cmd += " --load \"" + initFilePath + "\"";
|
||||
}
|
||||
cmd += " 2>&1";
|
||||
FILE* pipe = openPipe(cmd.c_str(), "r");
|
||||
if (!pipe) {
|
||||
lastError_ = "Failed to start Emacs daemon";
|
||||
return false;
|
||||
}
|
||||
char buf[512];
|
||||
while (fgets(buf, sizeof(buf), pipe)) {
|
||||
outLog += buf;
|
||||
}
|
||||
int result = closePipe(pipe);
|
||||
if (result == 0) {
|
||||
daemonRunning_ = true;
|
||||
return true;
|
||||
}
|
||||
// Even if command "fails", check if daemon is actually alive
|
||||
// (Emacs daemon may return non-zero but still be running)
|
||||
if (isDaemonAlive()) {
|
||||
daemonRunning_ = true;
|
||||
return true;
|
||||
}
|
||||
lastError_ = "Failed to start Emacs daemon";
|
||||
lastError_ = outLog.empty() ? "Failed to start Emacs daemon" : outLog;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -196,6 +215,22 @@ private:
|
||||
std::string lastError_;
|
||||
};
|
||||
|
||||
// Resolve emacs config path to init.el
|
||||
static inline std::string resolveEmacsInitPath(const std::string& configPath) {
|
||||
if (configPath.empty()) return "";
|
||||
std::filesystem::path p(configPath);
|
||||
if (std::filesystem::exists(p)) {
|
||||
if (std::filesystem::is_directory(p)) {
|
||||
return (p / "init.el").string();
|
||||
}
|
||||
return p.string();
|
||||
}
|
||||
if (p.extension().string().empty()) {
|
||||
return (p / "init.el").string();
|
||||
}
|
||||
return p.string();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MockEmacsConnection — for testing without a real Emacs installation
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -211,6 +246,13 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool startDaemonWithConfig(const std::string& initFilePath, std::string& outLog) {
|
||||
lastInitPath_ = initFilePath;
|
||||
outLog = "Loaded " + initFilePath;
|
||||
daemonRunning_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isDaemonAlive() const { return daemonRunning_; }
|
||||
|
||||
std::string sendCommand(const std::string& elispCommand) {
|
||||
@@ -241,8 +283,10 @@ public:
|
||||
|
||||
// Test helper: get the last command sent
|
||||
std::string getLastSentCommand() const { return lastSentCommand_; }
|
||||
std::string getLastInitPath() const { return lastInitPath_; }
|
||||
|
||||
private:
|
||||
std::string lastError_;
|
||||
std::string lastSentCommand_;
|
||||
std::string lastInitPath_;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ struct ImportEditResult {
|
||||
bool changed = false;
|
||||
};
|
||||
|
||||
static inline std::vector<std::string> splitLines(const std::string& text) {
|
||||
static inline std::vector<std::string> importSplitLines(const std::string& text) {
|
||||
std::vector<std::string> lines;
|
||||
std::stringstream ss(text);
|
||||
std::string line;
|
||||
@@ -81,7 +81,7 @@ static inline ImportEditResult ensureImportPython(const std::string& text,
|
||||
line = "from " + library + " import " + symbol;
|
||||
}
|
||||
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::vector<std::string> imports;
|
||||
std::vector<std::string> rest;
|
||||
for (const auto& l : lines) {
|
||||
@@ -108,7 +108,7 @@ static inline ImportEditResult ensureImportJs(const std::string& text,
|
||||
if (dot != std::string::npos) name = name.substr(dot + 1);
|
||||
std::string line = "import { " + name + " } from '" + library + "';";
|
||||
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::vector<std::string> imports;
|
||||
std::vector<std::string> rest;
|
||||
for (const auto& l : lines) {
|
||||
@@ -134,7 +134,7 @@ static inline ImportEditResult ensureImportRust(const std::string& text,
|
||||
if (symbol.find("::") != std::string::npos) line = "use " + symbol + ";";
|
||||
else line = "use " + library + "::" + symbol + ";";
|
||||
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::vector<std::string> imports;
|
||||
std::vector<std::string> rest;
|
||||
for (const auto& l : lines) {
|
||||
@@ -157,7 +157,7 @@ static inline ImportEditResult ensureImportGo(const std::string& text,
|
||||
ImportEditResult out{ text, false };
|
||||
std::string path = "\"" + library + "\"";
|
||||
std::string line = "import " + path;
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
int pkgLine = -1;
|
||||
int importStart = -1;
|
||||
int importEnd = -1;
|
||||
@@ -228,7 +228,7 @@ static inline ImportEditResult ensureImportElisp(const std::string& text,
|
||||
const std::string& library) {
|
||||
ImportEditResult out{ text, false };
|
||||
std::string line = "(require '" + library + ")";
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
for (const auto& l : lines) {
|
||||
if (trimStr(l) == line) return out;
|
||||
}
|
||||
@@ -244,7 +244,7 @@ static inline ImportEditResult ensureImportCpp(const std::string& text,
|
||||
const std::string& library) {
|
||||
ImportEditResult out{ text, false };
|
||||
std::string line = "#include <" + library + ">";
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::vector<std::string> imports;
|
||||
std::vector<std::string> rest;
|
||||
for (const auto& l : lines) {
|
||||
@@ -281,7 +281,7 @@ static inline ImportEditResult ensureImport(const std::string& text,
|
||||
|
||||
static inline std::vector<ImportIssue> findUnusedImportsPython(const std::string& text) {
|
||||
std::vector<ImportIssue> issues;
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::string body;
|
||||
for (const auto& l : lines) {
|
||||
std::string t = trimStr(l);
|
||||
@@ -309,7 +309,7 @@ static inline std::vector<ImportIssue> findUnusedImportsPython(const std::string
|
||||
|
||||
static inline std::vector<ImportIssue> findUnusedImportsJs(const std::string& text) {
|
||||
std::vector<ImportIssue> issues;
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::string body;
|
||||
for (const auto& l : lines) {
|
||||
std::string t = trimStr(l);
|
||||
@@ -341,7 +341,7 @@ static inline std::vector<ImportIssue> findUnusedImportsJs(const std::string& te
|
||||
|
||||
static inline std::vector<ImportIssue> findUnusedImportsRust(const std::string& text) {
|
||||
std::vector<ImportIssue> issues;
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::string body;
|
||||
for (const auto& l : lines) {
|
||||
std::string t = trimStr(l);
|
||||
@@ -365,7 +365,7 @@ static inline std::vector<ImportIssue> findUnusedImportsRust(const std::string&
|
||||
|
||||
static inline std::vector<ImportIssue> findUnusedImportsGo(const std::string& text) {
|
||||
std::vector<ImportIssue> issues;
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::string body;
|
||||
for (const auto& l : lines) {
|
||||
std::string t = trimStr(l);
|
||||
@@ -389,7 +389,7 @@ static inline std::vector<ImportIssue> findUnusedImportsGo(const std::string& te
|
||||
|
||||
static inline std::vector<ImportIssue> findUnusedImportsElisp(const std::string& text) {
|
||||
std::vector<ImportIssue> issues;
|
||||
auto lines = splitLines(text);
|
||||
auto lines = importSplitLines(text);
|
||||
std::string body;
|
||||
for (const auto& l : lines) {
|
||||
std::string t = trimStr(l);
|
||||
|
||||
@@ -169,9 +169,9 @@ private:
|
||||
const char* home = std::getenv("USERPROFILE");
|
||||
if (!home) home = std::getenv("HOME");
|
||||
if (home) {
|
||||
emacsConfigPath_ = (std::filesystem::path(home) / ".emacs.d").string();
|
||||
emacsConfigPath_ = (std::filesystem::path(home) / ".emacs.d" / "init.el").string();
|
||||
} else {
|
||||
emacsConfigPath_ = ".emacs.d";
|
||||
emacsConfigPath_ = ".emacs.d/init.el";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -698,6 +698,7 @@ int main(int, char**) {
|
||||
ImGui::Begin("Settings", &state.showSettingsPanel);
|
||||
ImGui::PushFont(uiFont);
|
||||
bool settingsChanged = false;
|
||||
bool emacsConfigChanged = false;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
int fontSize = state.settings.getFontSize();
|
||||
@@ -776,6 +777,7 @@ int main(int, char**) {
|
||||
ImGui::SetNextItemWidth(320);
|
||||
if (InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable())) {
|
||||
settingsChanged = true;
|
||||
emacsConfigChanged = true;
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("LSP Servers", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
@@ -799,6 +801,9 @@ int main(int, char**) {
|
||||
if (settingsChanged) {
|
||||
state.saveSettingsToDisk();
|
||||
}
|
||||
if (emacsConfigChanged) {
|
||||
state.startEmacsDaemonFromSettings();
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -1848,7 +1853,8 @@ int main(int, char**) {
|
||||
ImGui::BeginChild("##problemsScroll", ImVec2(0, 0), false);
|
||||
auto diags = state.lsp ? state.lsp->getDiagnostics() : std::vector<LSPClient::Diagnostic>{};
|
||||
const auto& whetDiags = state.whetstoneDiagnostics;
|
||||
if (diags.empty() && whetDiags.empty()) {
|
||||
const auto& emacsDiags = state.emacsDiagnostics;
|
||||
if (diags.empty() && whetDiags.empty() && emacsDiags.empty()) {
|
||||
ImGui::TextDisabled("(no diagnostics)");
|
||||
} else {
|
||||
for (const auto& d : diags) {
|
||||
@@ -1888,6 +1894,12 @@ int main(int, char**) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& d : emacsDiags) {
|
||||
const char* sev = "Error";
|
||||
std::string label = "[" + std::string(sev) + "] " + d.message +
|
||||
" (" + d.uri + ")";
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopFont();
|
||||
|
||||
38
editor/tests/step141_test.cpp
Normal file
38
editor/tests/step141_test.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Step 141 TDD Test: Emacs daemon with user config path
|
||||
#include "EmacsIntegration.h"
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
if (cond) {
|
||||
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
// resolveEmacsInitPath uses directory -> init.el
|
||||
std::filesystem::path dir = std::filesystem::current_path() / "emacs_cfg_tmp";
|
||||
std::filesystem::create_directories(dir);
|
||||
std::string resolved = resolveEmacsInitPath(dir.string());
|
||||
expect(resolved.find("init.el") != std::string::npos, "resolve init.el", passed, failed);
|
||||
|
||||
// Mock emacs daemon startup with init path
|
||||
MockEmacsConnection mock;
|
||||
std::string log;
|
||||
bool ok = mock.startDaemonWithConfig("C:/init.el", log);
|
||||
expect(ok, "mock start daemon ok", passed, failed);
|
||||
expect(mock.getLastInitPath() == "C:/init.el", "init path passed", passed, failed);
|
||||
expect(log.find("Loaded") != std::string::npos, "startup log returned", passed, failed);
|
||||
|
||||
std::filesystem::remove_all(dir);
|
||||
|
||||
std::cout << "\n=== Step 141 Results: " << passed << " passed, " << failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -172,7 +172,7 @@ what's already available.
|
||||
For Emacs users: respect their init.el, load their packages, expose Emacs
|
||||
functionality through the Whetstone UI.
|
||||
|
||||
- [ ] **Step 141: Emacs daemon with user config**
|
||||
- [x] **Step 141: Emacs daemon with user config**
|
||||
Modify the Orchestrator's Emacs daemon to start with the user's init file.
|
||||
Setting: "Emacs Config Path" (defaults to `~/.emacs.d/init.el`).
|
||||
Daemon loads user's packages (MELPA, ELPA, straight.el, use-package).
|
||||
|
||||
Reference in New Issue
Block a user