Files
drharness/include/rebase_engine.hpp
darthrubik 7fe1656c7d Add edge case tests for history editing feature (issue 114)
- Add test for editing a message and removing all content (empty content)
- Add test for multiple consecutive edits to the same message (changes stack)
- Add test for editing messages with special characters
- Add test for very long message content
- Add test for editing assistant messages with tool_calls
- Add test for dropping all messages (empty history result)
- Add test for unicode characters in message content
- Add test for whitespace variations in rebase file
- Add test for comments and empty lines mixed with commands
- Add test for special characters and unicode in rebase content display
- Fix apply_rebase to preserve original content when only role is changed
- Add content_edited flag to RebaseLine to track explicit edits
2026-05-30 19:49:05 -05:00

84 lines
3.0 KiB
C++

#pragma once
#include "json_types.hpp"
#include <string>
#include <vector>
#include <expected>
// Forward declaration
struct Message;
enum class RebaseCommand { Pick, Edit, Drop };
// JSON serialization for RebaseCommand
inline void to_json(nlohmann::json& j, RebaseCommand cmd) {
switch (cmd) {
case RebaseCommand::Pick: j = "pick"; break;
case RebaseCommand::Edit: j = "edit"; break;
case RebaseCommand::Drop: j = "drop"; break;
}
}
template<>
inline std::expected<RebaseCommand, std::string> json_to<RebaseCommand>(const nlohmann::json& j) {
if (!j.is_string()) return std::unexpected("expected string for RebaseCommand");
std::string s = j.get<std::string>();
if (s == "pick") return RebaseCommand::Pick;
if (s == "edit") return RebaseCommand::Edit;
if (s == "drop") return RebaseCommand::Drop;
return std::unexpected("invalid RebaseCommand: '" + s + "'");
}
template<> struct has_json_to<RebaseCommand> : std::true_type {};
struct RebaseLine {
RebaseCommand command;
size_t index;
std::string role;
std::string content; // For edit commands, stores the new content
bool content_edited = false; // True if content was explicitly edited (even if empty)
};
DRH_DEFINE_TYPE_MIXED(RebaseLine, (command, index, role), (content))
class RebaseEngine {
public:
std::expected<std::vector<RebaseLine>, std::string> parse_rebase_file(
const std::string& content, size_t history_size) const;
std::expected<std::vector<Message>, std::string> apply_rebase(
const std::vector<RebaseLine>& lines,
const std::vector<Message>& history) const;
};
class HistoryEditor {
public:
// Build the rebase editor content from a message history
// Each message is formatted as: pick N [role] first_line_of_content
// Tool calls display as: [tool_call: function_name]
// Empty messages display as: (empty)
std::string build_rebase_content(const std::vector<Message>& history) const;
// Build the edit prompt for editing a single message
// Shows previous message as context, separator, and current content to edit
// For message 0, shows "(no previous message)" instead of previous message
std::string build_edit_prompt(const std::vector<Message>& history,
size_t edit_index,
const std::string& role) const;
// Full interactive edit flow
// 1. Build rebase content and spawn editor
// 2. Parse and validate rebase commands
// 3. For each edit command, build edit prompt and spawn editor
// 4. Apply all changes
// Returns modified history on success, error message on failure
std::expected<std::vector<Message>, std::string> edit_history_interactive(
const std::vector<Message>& history) const;
private:
// Extract the first line of content from a message
std::string get_first_line(const std::string& content) const;
// Format display text for a single message
std::string get_message_display(const Message& msg) const;
};