diff --git a/src/cli/cli_main.cc b/src/cli/cli_main.cc index 50575583..1e9f30f4 100644 --- a/src/cli/cli_main.cc +++ b/src/cli/cli_main.cc @@ -30,6 +30,8 @@ struct ParsedGlobals { std::vector positional; bool show_help = false; bool show_version = false; + bool list_commands = false; + std::optional help_category; std::optional error; }; @@ -54,8 +56,22 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) { continue; } + if (absl::StartsWith(token, "--help=")) { + std::string category(token.substr(7)); + if (!category.empty()) { + result.help_category = category; + } else { + result.show_help = true; + } + continue; + } + if (token == "--help" || token == "-h") { - result.show_help = true; + if (i + 1 < argc && argv[i + 1][0] != '-') { + result.help_category = std::string(argv[++i]); + } else { + result.show_help = true; + } continue; } @@ -69,6 +85,23 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) { continue; } + if (token == "--list-commands" || token == "--list") { + result.list_commands = true; + continue; + } + + if (absl::StartsWith(token, "--quiet=")) { + std::string value(token.substr(8)); + bool enable = value.empty() || value == "1" || value == "true"; + absl::SetFlag(&FLAGS_quiet, enable); + continue; + } + + if (token == "--quiet" || token == "-q") { + absl::SetFlag(&FLAGS_quiet, true); + continue; + } + if (absl::StartsWith(token, "--rom=")) { absl::SetFlag(&FLAGS_rom, std::string(token.substr(6))); continue; @@ -200,6 +233,16 @@ int main(int argc, char* argv[]) { yaze::cli::ModernCLI cli; + if (globals.help_category.has_value()) { + cli.PrintCategoryHelp(*globals.help_category); + return EXIT_SUCCESS; + } + + if (globals.list_commands) { + cli.PrintCommandSummary(); + return EXIT_SUCCESS; + } + if (globals.show_help) { cli.PrintTopLevelHelp(); return EXIT_SUCCESS; diff --git a/src/cli/handlers/agent.cc b/src/cli/handlers/agent.cc index 2f6b874f..593ed90e 100644 --- a/src/cli/handlers/agent.cc +++ b/src/cli/handlers/agent.cc @@ -73,7 +73,7 @@ constexpr absl::string_view kUsage = " --ai_model= Model name (e.g., qwen2.5-coder:7b for Ollama)\n" " --ollama_host= Ollama server URL (default: http://localhost:11434)\n" " --gemini_api_key= Gemini API key (or set GEMINI_API_KEY env var)\n" - " --format= Output format: json | table | yaml\n" + " --format= Output format: text | markdown | json | compact\n" "\n" "For more details, see: docs/simple_chat_input_methods.md"; diff --git a/src/cli/handlers/agent/general_commands.cc b/src/cli/handlers/agent/general_commands.cc index 8c0a0bc4..8e480426 100644 --- a/src/cli/handlers/agent/general_commands.cc +++ b/src/cli/handlers/agent/general_commands.cc @@ -12,13 +12,14 @@ #include "absl/flags/declare.h" #include "absl/flags/flag.h" #include "absl/status/status.h" -#include "absl/status/statusor.h" +#include "absl/status/status.h" #include "absl/strings/ascii.h" #include "absl/strings/match.h" -#include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" -#include "absl/strings/str_replace.h" +#include "absl/strings/str_join.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" #include "absl/strings/string_view.h" #include "app/core/project.h" #include "app/zelda3/dungeon/room.h" @@ -533,6 +534,7 @@ absl::Status HandleSimpleChatCommand(const std::vector& arg_vec, std::optional batch_file; std::optional single_message; bool verbose = false; + std::optional format_option; for (size_t i = 0; i < arg_vec.size(); ++i) { const std::string& arg = arg_vec[i]; @@ -540,6 +542,16 @@ absl::Status HandleSimpleChatCommand(const std::vector& arg_vec, batch_file = arg.substr(7); } else if (arg == "--file" && i + 1 < arg_vec.size()) { batch_file = arg_vec[++i]; + } else if (absl::StartsWith(arg, "--format=")) { + format_option = arg.substr(9); + } else if (arg == "--format" && i + 1 < arg_vec.size()) { + format_option = arg_vec[++i]; + } else if (arg == "--json") { + format_option = "json"; + } else if (arg == "--markdown" || arg == "--md") { + format_option = "markdown"; + } else if (arg == "--compact" || arg == "--raw") { + format_option = "compact"; } else if (arg == "--verbose" || arg == "-v") { verbose = true; } else if (!absl::StartsWith(arg, "--") && !single_message.has_value()) { @@ -549,6 +561,23 @@ absl::Status HandleSimpleChatCommand(const std::vector& arg_vec, agent::AgentConfig config; config.verbose = verbose; + if (format_option.has_value()) { + std::string normalized = absl::AsciiStrToLower(*format_option); + if (normalized == "json") { + config.output_format = AgentOutputFormat::kJson; + } else if (normalized == "markdown" || normalized == "md") { + config.output_format = AgentOutputFormat::kMarkdown; + } else if (normalized == "compact" || normalized == "raw") { + config.output_format = AgentOutputFormat::kCompact; + } else if (normalized == "text" || normalized == "friendly" || + normalized == "pretty") { + config.output_format = AgentOutputFormat::kFriendly; + } else { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported chat format: ", *format_option, + ". Supported formats: text, markdown, json, compact")); + } + } SimpleChatSession session; session.SetConfig(config); diff --git a/src/cli/modern_cli.cc b/src/cli/modern_cli.cc index d52ed78f..82aac8e2 100644 --- a/src/cli/modern_cli.cc +++ b/src/cli/modern_cli.cc @@ -1,9 +1,12 @@ #include "cli/modern_cli.h" #include +#include #include "absl/flags/flag.h" #include "absl/flags/declare.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" @@ -13,6 +16,7 @@ #include "cli/z3ed_ascii_logo.h" ABSL_DECLARE_FLAG(std::string, rom); +ABSL_DECLARE_FLAG(bool, quiet); namespace yaze { namespace cli { @@ -114,6 +118,48 @@ void ModernCLI::SetupCommands() { } }; + commands_["chat"] = { + .name = "chat", + .description = + "Unified chat entrypoint with text, markdown, or JSON output", + .usage = + "z3ed chat [--mode=simple|gui|test] [--format=text|markdown|json|compact]" + " [--prompt \"\"] [--file=] [--quiet]", + .handler = [this](const std::vector& args) -> absl::Status { + return HandleChatEntryCommand(args); + } + }; + + commands_["proposal"] = { + .name = "proposal", + .description = "Review and manage AI-generated change proposals", + .usage = + "z3ed proposal [options]", + .handler = [this](const std::vector& args) -> absl::Status { + return HandleProposalCommand(args); + } + }; + + commands_["widget"] = { + .name = "widget", + .description = "Discover GUI widgets exposed through automation APIs", + .usage = "z3ed widget discover [--window=] [--type=]", + .handler = [this](const std::vector& args) -> absl::Status { + return HandleWidgetCommand(args); + } + }; + + commands_["widget discover"] = { + .name = "widget discover", + .description = "Inspect UI widgets using the automation service", + .usage = + "z3ed widget discover [--window=] [--type=]" + " [--format=text|json]", + .handler = [this](const std::vector& args) -> absl::Status { + return HandleWidgetCommand(args); + } + }; + commands_["project build"] = { .name = "project build", .description = "Build the project and create a new ROM file", @@ -305,11 +351,25 @@ void ModernCLI::ShowHelp() { std::cout << "\033[1m\033[36mCOMMANDS:\033[0m" << std::endl; std::cout << std::endl; - std::cout << " \033[1m🤖 AI Agent\033[0m" << std::endl; - std::cout << " agent simple-chat Natural language ROM queries" << std::endl; - std::cout << " agent test-conversation Interactive testing mode" << std::endl; - std::cout << " \033[90m→ z3ed help agent\033[0m" << std::endl; + std::cout << " \033[1m🤖 AI Agent\033[0m" << std::endl; + std::cout << " chat Unified chat entrypoint (text/json/markdown)" << std::endl; + std::cout << " agent simple-chat Natural language ROM queries" << std::endl; + std::cout << " agent test-conversation Interactive testing mode" << std::endl; + std::cout << " \033[90m→ z3ed help chat, z3ed help agent\033[0m" << std::endl; std::cout << std::endl; + + std::cout << " \033[1m🧠 Proposals\033[0m" << std::endl; + std::cout << " proposal run Execute AI-driven sandbox plan" << std::endl; + std::cout << " proposal list Show pending proposals" << std::endl; + std::cout << " proposal diff Review latest sandbox diff" << std::endl; + std::cout << " proposal accept Apply sandbox changes" << std::endl; + std::cout << " \033[90m→ z3ed help proposal\033[0m" << std::endl; + std::cout << std::endl; + + std::cout << " \033[1m🪟 GUI Automation\033[0m" << std::endl; + std::cout << " widget discover Inspect GUI widgets via automation" << std::endl; + std::cout << " \033[90m→ z3ed help widget\033[0m" << std::endl; + std::cout << std::endl; std::cout << " \033[1m🔧 ROM Patching\033[0m" << std::endl; std::cout << " patch apply-asar Apply Asar 65816 assembly patch" << std::endl; @@ -350,7 +410,7 @@ void ModernCLI::ShowHelp() { std::cout << "\033[1m\033[36mQUICK START:\033[0m" << std::endl; std::cout << " z3ed --tui" << std::endl; - std::cout << " z3ed agent simple-chat \"What is room 5?\" --rom=zelda3.sfc" << std::endl; + std::cout << " z3ed chat \"What is room 5?\" --rom=zelda3.sfc --format=markdown" << std::endl; std::cout << " z3ed patch apply-asar patch.asm --rom=zelda3.sfc" << std::endl; std::cout << std::endl; @@ -361,6 +421,14 @@ void ModernCLI::PrintTopLevelHelp() const { const_cast(this)->ShowHelp(); } +void ModernCLI::PrintCategoryHelp(const std::string& category) const { + const_cast(this)->ShowCategoryHelp(category); +} + +void ModernCLI::PrintCommandSummary() const { + const_cast(this)->ShowCommandSummary(); +} + void ModernCLI::ShowCategoryHelp(const std::string& category) { std::cout << GetColoredLogo() << std::endl; std::cout << std::endl; @@ -395,6 +463,54 @@ void ModernCLI::ShowCategoryHelp(const std::string& category) { std::cout << " • Use --ai_provider=gemini or --ai_provider=ollama" << std::endl; std::cout << std::endl; + } else if (category == "chat") { + std::cout << "\033[1m\033[36m💬 CHAT ENTRYPOINT\033[0m" << std::endl; + std::cout << std::endl; + std::cout << "\033[1mDESCRIPTION:\033[0m" << std::endl; + std::cout << " Launch the embedded agent in text, markdown, or JSON-friendly modes." << std::endl; + std::cout << std::endl; + std::cout << "\033[1mMODES:\033[0m" << std::endl; + std::cout << " chat --mode=simple Quick REPL with --format=text|markdown|json|compact" << std::endl; + std::cout << " chat --mode=batch --file=F Run prompts from file (one per line)" << std::endl; + std::cout << " chat --mode=gui Launch the full FTXUI conversation experience" << std::endl; + std::cout << " chat --mode=test Execute scripted agent conversation for QA" << std::endl; + std::cout << std::endl; + std::cout << "\033[1mOPTIONS:\033[0m" << std::endl; + std::cout << " --format=text|markdown|json|compact Control response formatting" << std::endl; + std::cout << " --prompt \"\" Send a single message and exit" << std::endl; + std::cout << " --file questions.txt Batch mode input" << std::endl; + std::cout << " --quiet Suppress extra banners" << std::endl; + std::cout << std::endl; + + } else if (category == "proposal") { + std::cout << "\033[1m\033[36m🧠 PROPOSAL WORKFLOWS\033[0m" << std::endl; + std::cout << std::endl; + std::cout << "\033[1mCOMMANDS:\033[0m" << std::endl; + std::cout << " proposal run --prompt \"\" Plan and execute changes in sandbox" << std::endl; + std::cout << " proposal list Show pending proposals" << std::endl; + std::cout << " proposal diff [--proposal-id=X] Inspect latest diff/log" << std::endl; + std::cout << " proposal accept --proposal-id=X Apply sandbox changes to main ROM" << std::endl; + std::cout << " proposal commit | proposal revert Persist or undo sandbox changes" << std::endl; + std::cout << std::endl; + std::cout << "\033[1mTIPS:\033[0m" << std::endl; + std::cout << " • Run `z3ed proposal list` frequently to monitor progress" << std::endl; + std::cout << " • Use `--prompt` to describe tasks in natural language" << std::endl; + std::cout << " • Sandbox artifacts live alongside proposal logs" << std::endl; + std::cout << std::endl; + + } else if (category == "widget") { + std::cout << "\033[1m\033[36m🪟 GUI WIDGET DISCOVERY\033[0m" << std::endl; + std::cout << std::endl; + std::cout << "\033[1mCOMMANDS:\033[0m" << std::endl; + std::cout << " widget discover [--window=] [--type=]" << std::endl; + std::cout << " Enumerate UI widgets available through automation hooks" << std::endl; + std::cout << " Options: --format=table|json, --limit , --include-invisible, --include-disabled" << std::endl; + std::cout << std::endl; + std::cout << "\033[1mTIPS:\033[0m" << std::endl; + std::cout << " • Requires the YAZE GUI to be running locally" << std::endl; + std::cout << " • Combine with `z3ed proposal run` for automated UI tests" << std::endl; + std::cout << std::endl; + } else if (category == "patch") { std::cout << "\033[1m\033[36m🔧 ROM PATCHING COMMANDS\033[0m" << std::endl; std::cout << std::endl; @@ -520,12 +636,44 @@ void ModernCLI::ShowCategoryHelp(const std::string& category) { } else { std::cout << "\033[1m\033[31mUnknown category: " << category << "\033[0m" << std::endl; std::cout << std::endl; - std::cout << "Available categories: agent, patch, rom, overworld, dungeon, gfx, palette" << std::endl; + std::cout << "Available categories: agent, chat, proposal, widget, patch, rom, overworld, dungeon, gfx, palette" << std::endl; std::cout << std::endl; std::cout << "Use 'z3ed --help' to see all commands." << std::endl; } } +void ModernCLI::ShowCommandSummary() const { + std::cout << GetColoredLogo() << std::endl; + std::cout << std::endl; + std::cout << "\033[1m\033[36mCOMMANDS OVERVIEW\033[0m" << std::endl; + std::cout << std::endl; + + for (const auto& [key, info] : commands_) { + std::string headline = info.description; + const size_t newline_pos = headline.find('\n'); + if (newline_pos != std::string::npos) { + headline = headline.substr(0, newline_pos); + } + if (headline.empty()) { + headline = info.usage; + } + if (headline.size() > 80) { + headline = absl::StrCat(headline.substr(0, 77), "…"); + } + + std::string label = info.name; + if (label.size() > 24) { + label = absl::StrCat(label.substr(0, 23), "…"); + } + + std::cout << " " + << absl::StrFormat("%-24s%s", label, headline) << std::endl; + } + + std::cout << std::endl; + std::cout << "Use \033[90mz3ed help \033[0m for detailed information." << std::endl; +} + absl::Status ModernCLI::Run(int argc, char* argv[]) { if (argc < 2) { ShowHelp(); @@ -737,8 +885,143 @@ absl::Status ModernCLI::HandleOverworldSetTileCommand(const std::vector& args) { - SpriteCreate handler; - return handler.Run(args); + SpriteCreate handler; + return handler.Run(args); +} + +absl::Status ModernCLI::HandleChatEntryCommand( + const std::vector& args) { + std::string mode = "simple"; + std::optional prompt; + std::vector forwarded; + + for (size_t i = 0; i < args.size(); ++i) { + const std::string& token = args[i]; + if (absl::StartsWith(token, "--mode=")) { + mode = absl::AsciiStrToLower(token.substr(7)); + continue; + } + if (token == "--mode" && i + 1 < args.size()) { + mode = absl::AsciiStrToLower(args[i + 1]); + ++i; + continue; + } + if (absl::StartsWith(token, "--prompt=")) { + prompt = token.substr(9); + continue; + } + if (token == "--prompt" && i + 1 < args.size()) { + prompt = args[i + 1]; + ++i; + continue; + } + if (token == "--quiet" || token == "-q") { + absl::SetFlag(&FLAGS_quiet, true); + continue; + } + if (!absl::StartsWith(token, "--") && !prompt.has_value()) { + prompt = token; + continue; + } + forwarded.push_back(token); + } + + const std::string normalized_mode = absl::AsciiStrToLower(mode); + + auto has_batch_file = [&forwarded]() { + for (const auto& token : forwarded) { + if (absl::StartsWith(token, "--file") || token == "--file") { + return true; + } + } + return false; + }; + + std::vector agent_args; + if (normalized_mode == "gui" || normalized_mode == "visual" || + normalized_mode == "tui") { + if (prompt.has_value()) { + return absl::InvalidArgumentError( + "GUI chat mode launches the interactive TUI and does not accept a --prompt value."); + } + agent_args.push_back("chat"); + } else if (normalized_mode == "test" || normalized_mode == "qa") { + if (prompt.has_value()) { + return absl::InvalidArgumentError( + "Test conversation mode does not accept an inline prompt."); + } + agent_args.push_back("test-conversation"); + } else { + if (normalized_mode == "batch" && !has_batch_file()) { + return absl::InvalidArgumentError( + "Batch chat mode requires a --file= option."); + } + agent_args.push_back("simple-chat"); + if (prompt.has_value()) { + agent_args.push_back(*prompt); + } + } + + agent_args.insert(agent_args.end(), forwarded.begin(), forwarded.end()); + return HandleAgentCommand(agent_args); +} + +absl::Status ModernCLI::HandleProposalCommand( + const std::vector& args) { + if (args.empty()) { + ShowCategoryHelp("proposal"); + return absl::OkStatus(); + } + + std::string subcommand = absl::AsciiStrToLower(args[0]); + std::vector forwarded(args.begin() + 1, args.end()); + std::vector agent_args; + + if (subcommand == "run" || subcommand == "plan") { + agent_args.push_back(subcommand); + } else if (subcommand == "list") { + agent_args.push_back("list"); + } else if (subcommand == "diff" || subcommand == "show") { + agent_args.push_back("diff"); + } else if (subcommand == "accept") { + agent_args.push_back("accept"); + } else if (subcommand == "commit") { + agent_args.push_back("commit"); + } else if (subcommand == "revert" || subcommand == "reject") { + agent_args.push_back("revert"); + } else if (subcommand == "test") { + agent_args.push_back("test"); + } else { + return absl::InvalidArgumentError( + absl::StrCat("Unknown proposal command: ", subcommand, + ". Valid actions: run, plan, list, diff, show, accept, commit, revert.")); + } + + agent_args.insert(agent_args.end(), forwarded.begin(), forwarded.end()); + return HandleAgentCommand(agent_args); +} + +absl::Status ModernCLI::HandleWidgetCommand( + const std::vector& args) { + if (args.empty()) { + ShowCategoryHelp("widget"); + return absl::OkStatus(); + } + + std::vector forwarded(args.begin(), args.end()); + std::string subcommand = absl::AsciiStrToLower(forwarded[0]); + std::vector agent_args; + + if (subcommand == "discover") { + agent_args.push_back("gui"); + agent_args.insert(agent_args.end(), forwarded.begin(), forwarded.end()); + } else { + return absl::InvalidArgumentError( + absl::StrCat("Unknown widget command: ", forwarded[0], + ". Try 'z3ed widget discover'.")); + } + + return HandleAgentCommand(agent_args); } } // namespace cli diff --git a/src/cli/modern_cli.h b/src/cli/modern_cli.h index 8a5bb12f..188d58fe 100644 --- a/src/cli/modern_cli.h +++ b/src/cli/modern_cli.h @@ -25,6 +25,8 @@ class ModernCLI { absl::Status Run(int argc, char* argv[]); CommandHandler* GetCommandHandler(const std::string& name); void PrintTopLevelHelp() const; + void PrintCategoryHelp(const std::string& category) const; + void PrintCommandSummary() const; std::map commands_; @@ -32,6 +34,7 @@ class ModernCLI { void SetupCommands(); void ShowHelp(); void ShowCategoryHelp(const std::string& category); + void ShowCommandSummary() const; // Command Handlers absl::Status HandleAsarPatchCommand(const std::vector& args); @@ -58,6 +61,9 @@ class ModernCLI { absl::Status HandleOverworldListWarpsCommand(const std::vector& args); absl::Status HandleOverworldSetTileCommand(const std::vector& args); absl::Status HandleSpriteCreateCommand(const std::vector& args); + absl::Status HandleChatEntryCommand(const std::vector& args); + absl::Status HandleProposalCommand(const std::vector& args); + absl::Status HandleWidgetCommand(const std::vector& args); }; } // namespace cli diff --git a/src/cli/service/agent/conversational_agent_service.h b/src/cli/service/agent/conversational_agent_service.h index 8cbdf550..dd84ed85 100644 --- a/src/cli/service/agent/conversational_agent_service.h +++ b/src/cli/service/agent/conversational_agent_service.h @@ -41,6 +41,13 @@ struct ChatMessage { std::optional metrics; }; +enum class AgentOutputFormat { + kFriendly, + kCompact, + kMarkdown, + kJson +}; + struct AgentConfig { int max_tool_iterations = 4; // Maximum number of tool calling iterations int max_retry_attempts = 3; // Maximum retries on errors @@ -48,6 +55,7 @@ struct AgentConfig { bool show_reasoning = true; // Show LLM reasoning in output size_t max_history_messages = 50; // Maximum stored history messages per session bool trim_history = true; // Whether to trim history beyond the limit + AgentOutputFormat output_format = AgentOutputFormat::kFriendly; }; class ConversationalAgentService { diff --git a/src/cli/service/agent/simple_chat_session.cc b/src/cli/service/agent/simple_chat_session.cc index 868e9f8c..d9fa3609 100644 --- a/src/cli/service/agent/simple_chat_session.cc +++ b/src/cli/service/agent/simple_chat_session.cc @@ -1,8 +1,10 @@ #include "cli/service/agent/simple_chat_session.h" +#include #include -#include #include +#include +#include #ifdef _WIN32 #include @@ -12,7 +14,9 @@ #include #endif +#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" #include "absl/time/time.h" #include "cli/util/terminal_colors.h" @@ -26,6 +30,170 @@ void SimpleChatSession::SetRomContext(Rom* rom) { agent_service_.SetRomContext(rom); } +namespace { + +std::string EscapeJsonString(absl::string_view input) { + std::string output; + output.reserve(input.size()); + for (char ch : input) { + switch (ch) { + case '\\': + output.append("\\\\"); + break; + case '"': + output.append("\\\""); + break; + case '\b': + output.append("\\b"); + break; + case '\f': + output.append("\\f"); + break; + case '\n': + output.append("\\n"); + break; + case '\r': + output.append("\\r"); + break; + case '\t': + output.append("\\t"); + break; + default: { + unsigned char code = static_cast(ch); + if (code < 0x20) { + absl::StrAppendFormat(&output, "\\u%04x", + static_cast(code)); + } else { + output.push_back(ch); + } + break; + } + } + } + return output; +} + +std::string QuoteJson(absl::string_view value) { + return absl::StrCat("\"", EscapeJsonString(value), "\""); +} + +std::string TableToJson(const ChatMessage::TableData& table) { + std::vector header_entries; + header_entries.reserve(table.headers.size()); + for (const auto& header : table.headers) { + header_entries.push_back(QuoteJson(header)); + } + + std::vector row_entries; + row_entries.reserve(table.rows.size()); + for (const auto& row : table.rows) { + std::vector cell_entries; + cell_entries.reserve(row.size()); + for (const auto& cell : row) { + cell_entries.push_back(QuoteJson(cell)); + } + row_entries.push_back( + absl::StrCat("[", absl::StrJoin(cell_entries, ","), "]")); + } + + return absl::StrCat("{\"headers\":[", absl::StrJoin(header_entries, ","), + "],\"rows\":[", absl::StrJoin(row_entries, ","), + "]}"); +} + +std::string MetricsToJson(const ChatMessage::SessionMetrics& metrics) { + return absl::StrCat( + "{\"turn_index\":", metrics.turn_index, "," + "\"total_user_messages\":", metrics.total_user_messages, "," + "\"total_agent_messages\":", metrics.total_agent_messages, "," + "\"total_tool_calls\":", metrics.total_tool_calls, "," + "\"total_commands\":", metrics.total_commands, "," + "\"total_proposals\":", metrics.total_proposals, "," + "\"total_elapsed_seconds\":", metrics.total_elapsed_seconds, "," + "\"average_latency_seconds\":", metrics.average_latency_seconds, "}"); +} + +std::string MessageToJson(const ChatMessage& msg, bool show_timestamp) { + std::string json = "{"; + absl::StrAppend(&json, "\"sender\":\"", + msg.sender == ChatMessage::Sender::kUser ? "user" + : "agent", + "\""); + + absl::StrAppend(&json, ",\"message\":", QuoteJson(msg.message)); + + if (msg.json_pretty.has_value()) { + absl::StrAppend(&json, ",\"structured\":", + QuoteJson(msg.json_pretty.value())); + } + + if (msg.table_data.has_value()) { + absl::StrAppend(&json, ",\"table\":", TableToJson(*msg.table_data)); + } + + if (msg.metrics.has_value()) { + absl::StrAppend(&json, ",\"metrics\":", + MetricsToJson(*msg.metrics)); + } + + if (show_timestamp) { + std::string timestamp = + absl::FormatTime("%Y-%m-%dT%H:%M:%S%z", msg.timestamp, + absl::LocalTimeZone()); + absl::StrAppend(&json, ",\"timestamp\":", QuoteJson(timestamp)); + } + + absl::StrAppend(&json, "}"); + return json; +} + +void PrintMarkdownTable(const ChatMessage::TableData& table) { + if (table.headers.empty()) { + return; + } + + std::cout << "\n|"; + for (const auto& header : table.headers) { + std::cout << " " << header << " |"; + } + std::cout << "\n|"; + for (size_t i = 0; i < table.headers.size(); ++i) { + std::cout << " --- |"; + } + std::cout << "\n"; + + for (const auto& row : table.rows) { + std::cout << "|"; + for (size_t i = 0; i < table.headers.size(); ++i) { + if (i < row.size()) { + std::cout << " " << row[i]; + } + std::cout << " |"; + } + std::cout << "\n"; + } +} + +void PrintMarkdownMetrics(const ChatMessage::SessionMetrics& metrics) { + std::cout << "\n> _Turn " << metrics.turn_index + << ": users=" << metrics.total_user_messages + << ", agents=" << metrics.total_agent_messages + << ", tool-calls=" << metrics.total_tool_calls + << ", commands=" << metrics.total_commands + << ", proposals=" << metrics.total_proposals + << ", elapsed=" + << absl::StrFormat("%.2fs avg %.2fs", + metrics.total_elapsed_seconds, + metrics.average_latency_seconds) + << "_\n"; +} + +std::string SessionMetricsToJson(const ChatMessage::SessionMetrics& metrics) { + return MetricsToJson(metrics); +} + +} // namespace + void SimpleChatSession::PrintTable(const ChatMessage::TableData& table) { if (table.headers.empty()) return; @@ -62,37 +230,77 @@ void SimpleChatSession::PrintTable(const ChatMessage::TableData& table) { } } -void SimpleChatSession::PrintMessage(const ChatMessage& msg, bool show_timestamp) { - const char* sender = (msg.sender == ChatMessage::Sender::kUser) ? "You" : "Agent"; - - if (show_timestamp) { - std::string timestamp = absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone()); - std::cout << "[" << timestamp << "] "; - } - - std::cout << sender << ": "; - - if (msg.table_data.has_value()) { - std::cout << "\n"; - PrintTable(msg.table_data.value()); - } else if (msg.json_pretty.has_value()) { - std::cout << "\n" << msg.json_pretty.value() << "\n"; - } else { - std::cout << msg.message << "\n"; - } +void SimpleChatSession::PrintMessage(const ChatMessage& msg, + bool show_timestamp) { + switch (config_.output_format) { + case AgentOutputFormat::kFriendly: { + const char* sender = + (msg.sender == ChatMessage::Sender::kUser) ? "You" : "Agent"; - if (msg.metrics.has_value()) { - const auto& metrics = msg.metrics.value(); - std::cout << " 📊 Turn " << metrics.turn_index - << " summary — users: " << metrics.total_user_messages - << ", agents: " << metrics.total_agent_messages - << ", tools: " << metrics.total_tool_calls - << ", commands: " << metrics.total_commands - << ", proposals: " << metrics.total_proposals - << ", elapsed: " - << absl::StrFormat("%.2fs avg %.2fs", metrics.total_elapsed_seconds, - metrics.average_latency_seconds) - << "\n"; + if (show_timestamp) { + std::string timestamp = absl::FormatTime( + "%H:%M:%S", msg.timestamp, absl::LocalTimeZone()); + std::cout << "[" << timestamp << "] "; + } + + std::cout << sender << ": "; + + if (msg.table_data.has_value()) { + std::cout << "\n"; + PrintTable(msg.table_data.value()); + } else if (msg.json_pretty.has_value()) { + std::cout << "\n" << msg.json_pretty.value() << "\n"; + } else { + std::cout << msg.message << "\n"; + } + + if (msg.metrics.has_value()) { + const auto& metrics = msg.metrics.value(); + std::cout << " 📊 Turn " << metrics.turn_index + << " summary — users: " << metrics.total_user_messages + << ", agents: " << metrics.total_agent_messages + << ", tools: " << metrics.total_tool_calls + << ", commands: " << metrics.total_commands + << ", proposals: " << metrics.total_proposals + << ", elapsed: " + << absl::StrFormat("%.2fs avg %.2fs", + metrics.total_elapsed_seconds, + metrics.average_latency_seconds) + << "\n"; + } + break; + } + case AgentOutputFormat::kCompact: { + if (msg.json_pretty.has_value()) { + std::cout << msg.json_pretty.value() << "\n"; + } else if (msg.table_data.has_value()) { + PrintTable(msg.table_data.value()); + } else { + std::cout << msg.message << "\n"; + } + break; + } + case AgentOutputFormat::kMarkdown: { + std::cout << (msg.sender == ChatMessage::Sender::kUser ? "**You:** " + : "**Agent:** "); + if (msg.table_data.has_value()) { + PrintMarkdownTable(msg.table_data.value()); + } else if (msg.json_pretty.has_value()) { + std::cout << "\n```json\n" << msg.json_pretty.value() + << "\n```\n"; + } else { + std::cout << msg.message << "\n"; + } + + if (msg.metrics.has_value()) { + PrintMarkdownMetrics(*msg.metrics); + } + break; + } + case AgentOutputFormat::kJson: { + std::cout << MessageToJson(msg, show_timestamp) << std::endl; + break; + } } } @@ -116,7 +324,7 @@ absl::Status SimpleChatSession::RunInteractive() { // Check if stdin is a TTY (interactive) or a pipe/file bool is_interactive = isatty(fileno(stdin)); - if (is_interactive) { + if (is_interactive && config_.output_format == AgentOutputFormat::kFriendly) { std::cout << "Z3ED Agent Chat (Simple Mode)\n"; std::cout << "Type 'quit' or 'exit' to end the session.\n"; std::cout << "Type 'reset' to clear conversation history.\n"; @@ -125,14 +333,14 @@ absl::Status SimpleChatSession::RunInteractive() { std::string input; while (true) { - if (is_interactive) { + if (is_interactive && config_.output_format != AgentOutputFormat::kJson) { std::cout << "You: "; std::cout.flush(); // Ensure prompt is displayed before reading } if (!std::getline(std::cin, input)) { // EOF reached (piped input exhausted or Ctrl+D) - if (is_interactive) { + if (is_interactive && config_.output_format != AgentOutputFormat::kJson) { std::cout << "\n"; } break; @@ -143,31 +351,68 @@ absl::Status SimpleChatSession::RunInteractive() { if (input == "reset") { Reset(); - std::cout << "Conversation history cleared.\n\n"; + if (config_.output_format == AgentOutputFormat::kJson) { + std::cout << "{\"event\":\"history_cleared\"}" << std::endl; + } else if (config_.output_format == AgentOutputFormat::kMarkdown) { + std::cout << "> Conversation history cleared.\n\n"; + } else { + std::cout << "Conversation history cleared.\n\n"; + } continue; } auto result = agent_service_.SendMessage(input); if (!result.ok()) { - std::cerr << "Error: " << result.status().message() << "\n\n"; + if (config_.output_format == AgentOutputFormat::kJson) { + std::cout << absl::StrCat( + "{\"event\":\"error\",\"message\":", + QuoteJson(result.status().message()), "}") + << std::endl; + } else if (config_.output_format == AgentOutputFormat::kMarkdown) { + std::cout << "> **Error:** " << result.status().message() << "\n\n"; + } else if (config_.output_format == AgentOutputFormat::kCompact) { + std::cout << "error: " << result.status().message() << "\n"; + } else { + std::cerr << "Error: " << result.status().message() << "\n\n"; + } continue; } PrintMessage(result.value(), false); - std::cout << "\n"; + if (config_.output_format != AgentOutputFormat::kJson) { + std::cout << "\n"; + } } const auto metrics = agent_service_.GetMetrics(); - std::cout << "Session totals — turns: " << metrics.turn_index - << ", user messages: " << metrics.total_user_messages - << ", agent messages: " << metrics.total_agent_messages - << ", tool calls: " << metrics.total_tool_calls - << ", commands: " << metrics.total_commands - << ", proposals: " << metrics.total_proposals - << ", elapsed: " - << absl::StrFormat("%.2fs avg %.2fs\n\n", - metrics.total_elapsed_seconds, - metrics.average_latency_seconds); + if (config_.output_format == AgentOutputFormat::kJson) { + std::cout << absl::StrCat("{\"event\":\"session_summary\",\"metrics\":", + SessionMetricsToJson(metrics), "}") + << std::endl; + } else if (config_.output_format == AgentOutputFormat::kMarkdown) { + std::cout << "\n> **Session totals** "; + std::cout << "turns=" << metrics.turn_index << ", users=" + << metrics.total_user_messages << ", agents=" + << metrics.total_agent_messages << ", tools=" + << metrics.total_tool_calls << ", commands=" + << metrics.total_commands << ", proposals=" + << metrics.total_proposals << ", elapsed=" + << absl::StrFormat("%.2fs avg %.2fs", + metrics.total_elapsed_seconds, + metrics.average_latency_seconds) + << "\n\n"; + } else { + std::cout << "Session totals — turns: " << metrics.turn_index + << ", user messages: " << metrics.total_user_messages + << ", agent messages: " << metrics.total_agent_messages + << ", tool calls: " << metrics.total_tool_calls + << ", commands: " << metrics.total_commands + << ", proposals: " << metrics.total_proposals + << ", elapsed: " + << absl::StrFormat("%.2fs avg %.2fs\n\n", + metrics.total_elapsed_seconds, + metrics.average_latency_seconds); + } return absl::OkStatus(); } @@ -179,8 +424,12 @@ absl::Status SimpleChatSession::RunBatch(const std::string& input_file) { absl::StrFormat("Could not open file: %s", input_file)); } - std::cout << "Running batch session from: " << input_file << "\n"; - std::cout << "----------------------------------------\n\n"; + if (config_.output_format == AgentOutputFormat::kFriendly) { + std::cout << "Running batch session from: " << input_file << "\n"; + std::cout << "----------------------------------------\n\n"; + } else if (config_.output_format == AgentOutputFormat::kMarkdown) { + std::cout << "### Batch session: " << input_file << "\n\n"; + } std::string line; int line_num = 0; @@ -190,29 +439,70 @@ absl::Status SimpleChatSession::RunBatch(const std::string& input_file) { // Skip empty lines and comments if (line.empty() || line[0] == '#') continue; - std::cout << "Input [" << line_num << "]: " << line << "\n"; + if (config_.output_format == AgentOutputFormat::kFriendly) { + std::cout << "Input [" << line_num << "]: " << line << "\n"; + } else if (config_.output_format == AgentOutputFormat::kMarkdown) { + std::cout << "- **Input " << line_num << "**: " << line << "\n"; + } else if (config_.output_format == AgentOutputFormat::kJson) { + std::cout << absl::StrCat( + "{\"event\":\"batch_input\",\"index\":", + line_num, ",\"prompt\":", QuoteJson(line), "}") + << std::endl; + } auto result = agent_service_.SendMessage(line); if (!result.ok()) { - std::cerr << "Error: " << result.status().message() << "\n\n"; + if (config_.output_format == AgentOutputFormat::kJson) { + std::cout << absl::StrCat( + "{\"event\":\"error\",\"index\":", line_num, + ",\"message\":", + QuoteJson(result.status().message()), "}") + << std::endl; + } else if (config_.output_format == AgentOutputFormat::kMarkdown) { + std::cout << " - ⚠️ " << result.status().message() << "\n"; + } else if (config_.output_format == AgentOutputFormat::kCompact) { + std::cout << "error@" << line_num << ": " + << result.status().message() << "\n"; + } else { + std::cerr << "Error: " << result.status().message() << "\n\n"; + } continue; } PrintMessage(result.value(), false); - std::cout << "\n"; + if (config_.output_format != AgentOutputFormat::kJson) { + std::cout << "\n"; + } } const auto metrics = agent_service_.GetMetrics(); - std::cout << "Batch session totals — turns: " << metrics.turn_index - << ", user messages: " << metrics.total_user_messages - << ", agent messages: " << metrics.total_agent_messages - << ", tool calls: " << metrics.total_tool_calls - << ", commands: " << metrics.total_commands - << ", proposals: " << metrics.total_proposals - << ", elapsed: " - << absl::StrFormat("%.2fs avg %.2fs\n\n", - metrics.total_elapsed_seconds, - metrics.average_latency_seconds); + if (config_.output_format == AgentOutputFormat::kJson) { + std::cout << absl::StrCat("{\"event\":\"session_summary\",\"metrics\":", + SessionMetricsToJson(metrics), "}") + << std::endl; + } else if (config_.output_format == AgentOutputFormat::kMarkdown) { + std::cout << "\n> **Batch totals** turns=" << metrics.turn_index + << ", users=" << metrics.total_user_messages << ", agents=" + << metrics.total_agent_messages << ", tools=" + << metrics.total_tool_calls << ", commands=" + << metrics.total_commands << ", proposals=" + << metrics.total_proposals << ", elapsed=" + << absl::StrFormat("%.2fs avg %.2fs", + metrics.total_elapsed_seconds, + metrics.average_latency_seconds) + << "\n\n"; + } else { + std::cout << "Batch session totals — turns: " << metrics.turn_index + << ", user messages: " << metrics.total_user_messages + << ", agent messages: " << metrics.total_agent_messages + << ", tool calls: " << metrics.total_tool_calls + << ", commands: " << metrics.total_commands + << ", proposals: " << metrics.total_proposals + << ", elapsed: " + << absl::StrFormat("%.2fs avg %.2fs\n\n", + metrics.total_elapsed_seconds, + metrics.average_latency_seconds); + } return absl::OkStatus(); } diff --git a/src/cli/service/agent/simple_chat_session.h b/src/cli/service/agent/simple_chat_session.h index ddcafbdc..15238222 100644 --- a/src/cli/service/agent/simple_chat_session.h +++ b/src/cli/service/agent/simple_chat_session.h @@ -36,6 +36,7 @@ class SimpleChatSession { // Set agent configuration void SetConfig(const AgentConfig& config) { + config_ = config; agent_service_.SetConfig(config); } @@ -65,6 +66,7 @@ class SimpleChatSession { void PrintTable(const ChatMessage::TableData& table); ConversationalAgentService agent_service_; + AgentConfig config_; }; } // namespace agent