diff --git a/docs/bashrc.md b/docs/bashrc.md new file mode 100644 index 0000000..eb93c51 --- /dev/null +++ b/docs/bashrc.md @@ -0,0 +1,26 @@ +# What bashrc need to be for OSC 333 + +``` + +# --- OSC 333 marks for the multiplexer --- +_osc333_preexec() { + [[ -n "$_osc333_in_cmd" ]] && return + printf '\e]333;C\a' + _osc333_in_cmd=1 +} + +_osc333_postexec() { + local ec=$? + printf '\e]333;D;%d\a' "$ec" + unset _osc333_in_cmd +} + +trap '_osc333_preexec' DEBUG # «pre‑exec» +PROMPT_COMMAND='_osc333_postexec' # «post‑exec» + +PS1='\[\e]333;A\a\]\u@\h:\w\$ \[\e]333;B\a\]' +PS2='\[\e]333;A\a\]> \[\e]333;B\a\]' +PS3='\[\e]333;A\a\]#? \[\e]333;B\a\]' +``` + +description of a standart - https://gitlab.freedesktop.org/Per_Bothner/specifications/-/blob/master/proposals/semantic-prompts.md diff --git a/docs/terminal_multiplexer_handle_input.md b/docs/terminal_multiplexer_handle_input.md new file mode 100644 index 0000000..9782e66 --- /dev/null +++ b/docs/terminal_multiplexer_handle_input.md @@ -0,0 +1,12 @@ +## How `bash` window is working in `terminal_multiplexer.cpp` + +- Method `init` initialize two PTY's for agent and bash windows +- Methods from `refresh_cursor` to `resize` (lines 30-37 in `terminal_multiplexer.hpp`) are technical +- Method `run_terminal` creates an epoll instance and fill it with `STDIN`, screens PTY's and fd for window resize. After this it goes to infinite cycle for waiting this fd's. + +### Then `STDIN` was called + +- Symbols handles by `handle_input()` method for special symbols like `CTRL + B` and if it is just a letter, it writes this to fd of bash screen +- Then method `handle_screen_output(Screen &screen, const int fd)` starts working, because it handles everything that needs to print on a screen(including bash screen) +- Method `handle_screen_output(Screen &screen, const int fd)` also can be called from epolling cycle by bash command output +- This method reads all symbols from given `fd` and sends it to `screen's` method `handle_char(const TerminalChar &tch)` to show it on a screen \ No newline at end of file diff --git a/run.sh b/run.sh index 8ce827a..289ece9 100755 --- a/run.sh +++ b/run.sh @@ -3,24 +3,22 @@ PROJECT_DIR="tui-tux" DEPENDENCIES=( - "libncurses5-dev" - "libncursesw5-dev" - "libreadline-dev" - "libcurl4-openssl-dev" + "ncurses" + "readline" + "curl" ) JSON_DIR="tui-tux/nlohmann" RUN_TESTS=false is_package_installed() { - dpkg -s "$1" &> /dev/null + pacman -Qi "$1" &> /dev/null return $? } install_package() { echo "Installing $1..." - sudo apt-get update -qq - sudo apt-get install -y "$1" + sudo pacman -S --noconfirm --needed "$1" } if ! [ -f "$JSON_DIR/json.hpp" ]; then @@ -39,9 +37,9 @@ fi for arg in "$@"; do case $arg in -t|--test) - RUN_TESTS=true - shift - ;; + RUN_TESTS=true + shift + ;; esac done diff --git a/tui-tux/Makefile b/tui-tux/Makefile index 64a5c35..2cf5c34 100644 --- a/tui-tux/Makefile +++ b/tui-tux/Makefile @@ -2,12 +2,12 @@ TARGET := ishell TEST_TARGET := test_ishell # Configurable -NO_MAIN_SOURCES := screen.cpp escape.cpp agency_manager.cpp command_manager.cpp bookmark_manager.cpp agent.cpp terminal_multiplexer.cpp agency_request_wrapper.cpp https_client.cpp utils.cpp +NO_MAIN_SOURCES := screen.cpp escape.cpp agency_manager.cpp command_manager.cpp bookmark_manager.cpp agent.cpp terminal_multiplexer.cpp agency_request_wrapper.cpp https_client.cpp utils.cpp session_tracker.cpp markdown_manager.cpp SOURCES := $(NO_MAIN_SOURCES) main.cpp TEST_SOURCES := test_bookmark_manager.cpp test_agency_request_wrapper.cpp test_https_client.cpp test_escape.cpp test_agency_manager.cpp test_terminal_multiplexer.cpp test_command_manager.cpp FLAGS := -Wall -LIBS := -lncurses -lreadline -lcurl +LIBS := -lncurses -lreadline -lcurl -lsqlite3 TEST_EXTRA_LIBS := -lgtest -lgmock -L/usr/local/lib -lgtest_main -lpthread NO_MAIN_OBJECTS := $(patsubst %.cpp,%.o,$(patsubst %, bin/%, $(NO_MAIN_SOURCES))) @@ -20,7 +20,7 @@ TEST_SOURCES := $(patsubst %, test/%, $(TEST_SOURCES)) INCLUDE := -Iinclude -CXXFLAGS := $(FLAGS) -std=c++17 -g +CXXFLAGS := $(FLAGS) -std=c++17 -g3 Cxx := g++ diff --git a/tui-tux/include/agency_manager.hpp b/tui-tux/include/agency_manager.hpp index 7b5ed02..37301f9 100644 --- a/tui-tux/include/agency_manager.hpp +++ b/tui-tux/include/agency_manager.hpp @@ -11,7 +11,7 @@ class AgencyManager { virtual ~AgencyManager(); std::string get_agency_url(); - virtual std::string execute_query(const std::string &endpoint, const std::string &query); + virtual std::string execute_query(const std::string &endpoint, std::string query, bool include_session_history); [[nodiscard]] virtual std::string get_agent_name() const; virtual void set_agent_name(const std::string &agent_name); @@ -21,9 +21,6 @@ class AgencyManager { std::string agent_name = "assistant"; AgencyRequestWrapper* request_wrapper; - - // - std::vector> session_history; }; #endif // AGENCY_HPP diff --git a/tui-tux/include/agency_request_wrapper.hpp b/tui-tux/include/agency_request_wrapper.hpp index 24be9e5..1cedb4b 100644 --- a/tui-tux/include/agency_request_wrapper.hpp +++ b/tui-tux/include/agency_request_wrapper.hpp @@ -17,15 +17,19 @@ class AgencyRequestWrapper { virtual std::string get_ssh_ip(); virtual int get_ssh_port(); virtual std::string get_ssh_user(); - json send_request_to_agent_server(const std::string &url, const std::string &user_query, std::vector> &session_history); + json send_request_to_agent_server(const std::string &url, std::string user_query, bool include_session_history); + + static std::string get_session_history_string(); + + virtual std::string ask_agent(const std::string &url, std::string user_query, bool include_session_history); - static std::string get_session_history_string(std::vector> &session_history); - virtual std::string ask_agent(const std::string &url, const std::string &user_query, std::vector> &session_history); virtual json make_http_request(HttpRequestType request_type, const std::string& url, const std::map& query_params, const json& body, const std::map& headers); virtual char *getenv(const char *key); + + std::vector context; }; #endif // AGENCY_REQUEST_WRAPPER_HPP diff --git a/tui-tux/include/bookmark_manager.hpp b/tui-tux/include/bookmark_manager.hpp index f6a4a3b..cc0f9e2 100644 --- a/tui-tux/include/bookmark_manager.hpp +++ b/tui-tux/include/bookmark_manager.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "../nlohmann/json.hpp" #include @@ -17,7 +18,9 @@ class BookmarkManager { explicit BookmarkManager(AgencyManager *agency_manager); virtual ~BookmarkManager(); - virtual std::string get_bookmarks_file_path(); +// virtual std::string get_bookmarks_file_path(); + + std::unordered_map> bookmarks; virtual void load_bookmarks(const std::string &filename); virtual void save_bookmarks(const std::string &filename); @@ -27,9 +30,7 @@ class BookmarkManager { virtual void list_bookmarks() const; virtual void remove_bookmark(const std::string &alias); - virtual void bookmark(int index, const std::string &alias); - - std::unordered_map> bookmarks; + virtual void bookmark(int first, int last, const std::string& alias); virtual bool create_bookmarks_file(const std::string &filename); virtual void parse_bookmark_json(const json &bookmark); diff --git a/tui-tux/include/command_manager.hpp b/tui-tux/include/command_manager.hpp index 417462e..237d2b1 100644 --- a/tui-tux/include/command_manager.hpp +++ b/tui-tux/include/command_manager.hpp @@ -4,6 +4,7 @@ #include #include "bookmark_manager.hpp" +#include "markdown_manager.hpp" using json = nlohmann::json; @@ -11,21 +12,26 @@ class CommandManager { public: virtual ~CommandManager() = default; - explicit CommandManager(BookmarkManager *bookmark_manager); + explicit CommandManager(BookmarkManager *bookmark_manager, MarkdownManager *markdown_manager); void run_command(std::string &command); virtual void run_alias(std::string &alias); + void clear(const std::vector &args); void bookmark(const std::vector &args); void agent_switch(const std::vector &args); + void markdown(const std::vector &args); + virtual int read_from_file(std::string &filepath, std::string &output); BookmarkManager *bookmark_manager; + MarkdownManager *markdown_manager; std::unordered_map&)> command_map = { {"bookmark", &CommandManager::bookmark}, {"clear", &CommandManager::clear}, - {"switch", &CommandManager::agent_switch} + {"switch", &CommandManager::agent_switch}, + {"markdown", &CommandManager::markdown} }; }; diff --git a/tui-tux/include/escape.hpp b/tui-tux/include/escape.hpp index 013f5c2..1da39ff 100644 --- a/tui-tux/include/escape.hpp +++ b/tui-tux/include/escape.hpp @@ -38,6 +38,12 @@ // Insert character #define E_KEY_ICH 266 +// ────────── OSC 133 (semantic prompt marks) ────────── +#define E_OSC_PROMPT_START 512 // 333;A +#define E_OSC_PROMPT_END 513 // 333;B +#define E_OSC_PRE_EXEC 514 // 333;C +#define E_OSC_CMD_FINISH 515 // 333;D; + struct TerminalChar { int ch; std::vector args; diff --git a/tui-tux/include/markdown_manager.hpp b/tui-tux/include/markdown_manager.hpp new file mode 100644 index 0000000..f2e89f3 --- /dev/null +++ b/tui-tux/include/markdown_manager.hpp @@ -0,0 +1,42 @@ +#ifndef MARKDOWN_MANAGER_HPP +#define MARKDOWN_MANAGER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include "../nlohmann/json.hpp" +#include "bookmark_manager.hpp" +#include "agency_manager.hpp" + +using json = nlohmann::json; + +class MarkdownManager { + +public: + explicit MarkdownManager(); + virtual ~MarkdownManager(); + + virtual bool create_markdowns_file(const std::string &filename); + virtual void parse_markdown_json(const json &markdown); + + virtual void load_markdowns(const std::string &filename); + virtual void save_markdowns(const std::string &filename); + + virtual bool is_markdown(const std::string &bookmark) const; + virtual std::pair get_markdown(const std::string &bookmark) const; + + virtual void markdown(const std::pair& protocol); // create markdown from bookmark with "default" group + virtual void list_markdowns() const; // list markdowns "markdown -l" + virtual void remove_markdown(const std::string &bookmark); // remove markdown "markdown -r" + virtual void add_to_group(const std::string &group, const std::string &bookmark); // change markdown's group + virtual std::vector get_by_group(const std::string &group); // give LLM context of this group + virtual bool save_as_file(const std::string& bookmark); // save markdown as .md + + std::unordered_map> markdowns; // [] +}; + +#endif //MARKDOWN_MANAGER_HPP diff --git a/tui-tux/include/screen.hpp b/tui-tux/include/screen.hpp index 29c65ed..6894805 100644 --- a/tui-tux/include/screen.hpp +++ b/tui-tux/include/screen.hpp @@ -85,6 +85,8 @@ class Screen { int waddch(WINDOW *window, chtype ch); int winsch(WINDOW *window, chtype ch); int wmove(WINDOW *window, int y, int x); + + std::string get_line(int line_num); }; #endif \ No newline at end of file diff --git a/tui-tux/include/session_tracker.hpp b/tui-tux/include/session_tracker.hpp new file mode 100644 index 0000000..9b03402 --- /dev/null +++ b/tui-tux/include/session_tracker.hpp @@ -0,0 +1,63 @@ +#ifndef SESSION_TRACKER_HPP +#define SESSION_TRACKER_HPP + +#include +#include +#include + +class SessionTracker { +public: + enum class EventType { + ShellCommand, + SystemCommand, + Unknown + }; + + struct ShellCmd { + int interaction_id; + std::string command; + std::string output; + std::string execution_start; + std::string execution_end; + int exit_code; + }; + + struct Interaction { + int interaction_id; + std::string timestamp; + std::string question; + std::string answer; + std::vector shell; + }; + + static SessionTracker& get(); + + void startSession(); + void endSession(); + + void logAgentInteraction(const std::string& question, const std::string& response); + + void addNewCommand(EventType command_type); + void appendCommandText(const std::string& text); + void setCommandOutput(const std::string& output); + void setExitCode(int exit_code); + + const std::vector& get_history() const; + +private: + SessionTracker(); + ~SessionTracker(); + + SessionTracker(const SessionTracker&) = delete; + SessionTracker& operator=(const SessionTracker&) = delete; + + void openLogFile(); + + sqlite3 *db; + int sessionDbId; + int lastCommandId; + + mutable std::vector history; +}; + +#endif // SESSION_TRACKER_HPP diff --git a/tui-tux/include/terminal_multiplexer.hpp b/tui-tux/include/terminal_multiplexer.hpp index 10770b2..f124f5c 100644 --- a/tui-tux/include/terminal_multiplexer.hpp +++ b/tui-tux/include/terminal_multiplexer.hpp @@ -35,13 +35,20 @@ class TerminalMultiplexer { void send_dims(); void resize(); void run_terminal(); - int handle_screen_output(Screen &screen, int fd) const; + int handle_screen_output(Screen &screen, int fd); int handle_input(); static void handle_pty_input(int fd, char ch); void zoom_in(); void zoom_out(); void toggle_manual_scroll(); + + /* ────────── OSC 133 state (bash‑экран) ────────── */ + bool bash_capturing = false; + bool bash_waiting_for_prompt = true; + std::string bash_output; + int last_command_line = -10; + int first_command_line = -10; }; #endif \ No newline at end of file diff --git a/tui-tux/include/utils.hpp b/tui-tux/include/utils.hpp index 0823d8e..8ec4e75 100644 --- a/tui-tux/include/utils.hpp +++ b/tui-tux/include/utils.hpp @@ -19,10 +19,11 @@ #include #include -std::vector split(std::string &str, char delim, bool ignore_empty); +std::vector split(const std::string &str, char delim, bool ignore_empty); std::string join(std::vector &words, char delim); +bool is_range_token(const std::string& s); -#define DEFAULT_AGENCY_URL "https://ishell-stage.csai.site/agents" +#define DEFAULT_AGENCY_URL "http://142.93.231.126:5000/agents" #define DEFAULT_ISHELL_LOCAL_DIR "/etc/ishell" #endif \ No newline at end of file diff --git a/tui-tux/manuals/bookmark.txt b/tui-tux/manuals/bookmark.txt index 0ce68d2..7ca7841 100644 --- a/tui-tux/manuals/bookmark.txt +++ b/tui-tux/manuals/bookmark.txt @@ -1,8 +1,8 @@ Bookmark Command Help: Usage: - bookmark Bookmark the command at the given history index with the specified alias. - bookmark Bookmark the last executed command with the specified alias. + bookmark - Bookmark the command at the given history from firs index to the last with the specified alias. + bookmark Bookmark all executed command with the specified alias. bookmark -l List all saved bookmarks. bookmark --list List all saved bookmarks. bookmark -r Remove the bookmark with the specified alias. @@ -11,11 +11,11 @@ Usage: Show bookmark data for the specified alias. Options: - The index of the command in the history to be bookmarked. + - The first and last indexes of the agent command in the history to be bookmarked. Possible use without one of them. A unique name to identify the bookmarked command. Examples: - bookmark 3 myAlias Bookmark the command at history index 3 with the alias 'myAlias'. + bookmark 0-3 myAlias Bookmark the command at history from 1st to 3rd with the alias 'myAlias' and shell history before first. bookmark myAlias Bookmark the last executed command with the alias 'myAlias'. bookmark -l List all current bookmarks. bookmark -r myAlias Remove the bookmark with the alias 'myAlias'. diff --git a/tui-tux/schema.sql b/tui-tux/schema.sql new file mode 100644 index 0000000..3494384 --- /dev/null +++ b/tui-tux/schema.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS sessions ( + session_id INTEGER PRIMARY KEY AUTOINCREMENT, + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + end_time TIMESTAMP +); +CREATE TABLE IF NOT EXISTS interactions ( + interaction_id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + agent_question TEXT, + agent_message TEXT, + recommendation_id TEXT, + resolved BOOLEAN DEFAULT 0, + FOREIGN KEY(session_id) REFERENCES sessions(session_id) +); +CREATE TABLE IF NOT EXISTS commands ( + command_id INTEGER PRIMARY KEY AUTOINCREMENT, + interaction_id INTEGER, + command_text TEXT, + execution_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + execution_end TIMESTAMP, + exit_code INTEGER, + output TEXT, + shell_command BOOLEAN DEFAULT 0, + FOREIGN KEY(interaction_id) REFERENCES interactions(interaction_id) +); + diff --git a/tui-tux/src/agency_manager.cpp b/tui-tux/src/agency_manager.cpp index 6fa21b5..7c27645 100644 --- a/tui-tux/src/agency_manager.cpp +++ b/tui-tux/src/agency_manager.cpp @@ -30,9 +30,7 @@ void AgencyManager::set_agent_name(const std::string &agent_name) { this->agent_name = agent_name; } -std::string AgencyManager::execute_query(const std::string &endpoint, const std::string &query) { - std::string result = request_wrapper->ask_agent(endpoint, query, session_history); - session_history.emplace_back(query, result); - +std::string AgencyManager::execute_query(const std::string &endpoint, std::string query, bool include_session_history) { + std::string result = request_wrapper->ask_agent(endpoint, query, include_session_history); return result; } diff --git a/tui-tux/src/agency_request_wrapper.cpp b/tui-tux/src/agency_request_wrapper.cpp index eca5cd7..2bebf3d 100644 --- a/tui-tux/src/agency_request_wrapper.cpp +++ b/tui-tux/src/agency_request_wrapper.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include "../nlohmann/json.hpp" #include #include +#include #define INSTALLED_PACKAGES_BUFSIZ 128 @@ -14,11 +16,21 @@ using json = nlohmann::json; // Function to get Linux distribution std::string AgencyRequestWrapper::get_linux_distro() { - utsname buffer{}; - if (uname(&buffer) != 0) { - return "Unknown"; + std::ifstream f("/etc/os-release"); + if (f) { + std::string line; + while (std::getline(f, line)) { + if (line.rfind("ID=", 0) == 0) { + std::string id = line.substr(3); + if (!id.empty() && id.front() == '"') id.erase(0, 1); + if (!id.empty() && id.back() == '"') id.pop_back(); + return id; // arch, debian, ubuntu, fedora, … + } + } } - return std::string(buffer.sysname) + std::string(" ") + std::string(buffer.version); // or use buffer.release, buffer.version + // fallback → uname + utsname buf{}; + return (uname(&buf) == 0) ? std::string(buf.sysname) : "Unknown"; } // Function to get installed packages (simple example using dpkg on Debian/Ubuntu systems) @@ -61,13 +73,23 @@ std::string AgencyRequestWrapper::get_ssh_user() { } // Function to send request to agent's server -json AgencyRequestWrapper::send_request_to_agent_server(const std::string& url, const std::string& user_query, std::vector> &session_history) { +json AgencyRequestWrapper::send_request_to_agent_server(const std::string& url, std::string user_query, bool include_session_history) { std::string distro = get_linux_distro(); std::vector installed_packages = get_installed_packages(); std::string ssh_ip = get_ssh_ip(); int ssh_port = get_ssh_port(); std::string ssh_user = get_ssh_user(); - std::string history = get_session_history_string(session_history); + std::string history = ""; + if(include_session_history){ + history = get_session_history_string(); + } + + if(context.size() > 0){ + user_query += "\n\n"; + for(auto doc : context){ + user_query += doc + "\n"; + } + } const json request_body = { {"distro", distro}, @@ -94,14 +116,37 @@ json AgencyRequestWrapper::send_request_to_agent_server(const std::string& url, return response; } +std::string AgencyRequestWrapper::get_session_history_string() { + const auto& hist = SessionTracker::get().get_history(); // vector + + std::ostringstream buf; + + for (unsigned int i = 0; i < hist.size(); i++) + { + const auto& inter = hist[i]; + buf << '[' << inter.timestamp << "] Query: " << inter.question << '\n' + << "Answer: " << inter.answer << "\n\n"; + + for (const auto& cmd : inter.shell) + { + buf << '[' << cmd.execution_start << "] " << cmd.command << '\n' + << cmd.output << '\n' + << "[exit code: " << cmd.exit_code + << ", finished " << cmd.execution_end << "]\n\n"; + } + buf << "----------------------------------------\n"; + } + return buf.str(); +} + // Wrapper prep function for request for agent -std::string AgencyRequestWrapper::ask_agent(const std::string& url, const std::string& user_query, std::vector> &session_history) { - json response = send_request_to_agent_server(url, user_query, session_history); +std::string AgencyRequestWrapper::ask_agent(const std::string& url, std::string user_query, bool include_session_history) { + json response = send_request_to_agent_server(url, user_query, include_session_history); const json response_body = response["body"]; if (response_body.contains("error")) { - std::cerr << "Request failed: " << response_body["error"] << std::endl; +// std::cerr << "Request failed: " << response_body["error"] << std::endl; return ""; } @@ -110,6 +155,7 @@ std::string AgencyRequestWrapper::ask_agent(const std::string& url, const std::s } std::cerr << "\"content\" field not found in response body" << std::endl; + std::cerr << response.dump(2) << '\n'; return ""; } @@ -123,12 +169,4 @@ json AgencyRequestWrapper::make_http_request(const HttpRequestType request_type, char *AgencyRequestWrapper::getenv(const char *key) { return std::getenv(key); -} - -std::string AgencyRequestWrapper::get_session_history_string(std::vector> &session_history) { - std::ostringstream history_stream; - for (const auto &[fst, snd] : session_history) { - history_stream << "Query: " << fst << "\nAnswer: " << snd << "\n"; - } - return history_stream.str(); } \ No newline at end of file diff --git a/tui-tux/src/agent.cpp b/tui-tux/src/agent.cpp index 7a14514..333126e 100644 --- a/tui-tux/src/agent.cpp +++ b/tui-tux/src/agent.cpp @@ -1,13 +1,17 @@ -#include -#include #include #include #include #include #include + +#include +#include + #include #include +#include #include +#include #include "command_manager.hpp" @@ -21,7 +25,8 @@ std::vector> history_storage; // stores histories AgencyRequestWrapper request_wrapper; AgencyManager manager(&request_wrapper); BookmarkManager bookmark_manager(&manager); -CommandManager command_manager(&bookmark_manager); +MarkdownManager markdown_manager; +CommandManager command_manager(&bookmark_manager, &markdown_manager); bool running = true; int prompt_mode = MODE_AGENT; @@ -52,8 +57,10 @@ void line_handler(char *line) { if (prompt_mode == MODE_AGENT) { // New query to agent const std::string agency = manager.get_agency_url(); - const std::string result = manager.execute_query(agency + "/" + bookmark_manager.agency_manager->get_agent_name(), input_str); + const std::string result = manager.execute_query(agency + "/" + bookmark_manager.agency_manager->get_agent_name(), input_str, true); std::cout << result << "\n"; + + SessionTracker::get().logAgentInteraction(input_str, result); } else if (prompt_mode == MODE_SYSTEM) { command_manager.run_command(input_str); } @@ -64,6 +71,7 @@ void line_handler(char *line) { void agent() { // Enable history storage history_storage = std::vector(2, std::vector()); + SessionTracker::get().startSession(); // Disable TAB-completion. This was impossible to find rl_inhibit_completion = 1; @@ -72,6 +80,7 @@ void agent() { manager.get_agency_url(); bookmark_manager.load_bookmarks("local/bookmarks.json"); + markdown_manager.load_markdowns("local/markdowns.json"); const std::string system_name = "system"; @@ -105,4 +114,6 @@ void agent() { } bookmark_manager.save_bookmarks("local/bookmarks.json"); -} + markdown_manager.save_markdowns("local/markdowns.json"); + SessionTracker::get().endSession(); +} \ No newline at end of file diff --git a/tui-tux/src/bookmark_manager.cpp b/tui-tux/src/bookmark_manager.cpp index 55766e0..bcdc1da 100644 --- a/tui-tux/src/bookmark_manager.cpp +++ b/tui-tux/src/bookmark_manager.cpp @@ -2,10 +2,12 @@ #include #include #include +#include #include #include "../nlohmann/json.hpp" #include #include +#include #include using json = nlohmann::json; @@ -15,7 +17,7 @@ BookmarkManager::BookmarkManager(AgencyManager* agency_manager) { } BookmarkManager::~BookmarkManager() = default; - +/* std::string BookmarkManager::get_bookmarks_file_path() { std::string file_path; if (char *local_dir = getenv("ISHELL_LOCAL_DIR"); local_dir == nullptr) { @@ -41,7 +43,7 @@ std::string BookmarkManager::get_bookmarks_file_path() { return file_path; } - +*/ bool BookmarkManager::create_bookmarks_file(const std::string &filename) { std::ofstream create_file(filename); if (!create_file) { @@ -103,25 +105,72 @@ std::pair BookmarkManager::get_bookmark(const std::str return {"", ""}; // Return an empty pair if not found } -void BookmarkManager::bookmark(int index, const std::string &alias) { - // bookmark already exists +void BookmarkManager::bookmark(int first, int last, const std::string& alias){ + // bookmark already exists if (bookmarks.find(alias) != bookmarks.end()) { std::cerr << "Error: Bookmark '" << alias << "' already exists.\n"; return; } - // find pair - const int len = static_cast(agency_manager->session_history.size()); - if (index <= 0 || index > len) { - std::cerr << "Error: Invalid history index.\n"; + const auto& hist = SessionTracker::get().get_history(); // vector + if (hist.empty()) { + std::cerr << "Nothing to bookmark – history is empty\n"; + return; + } + + std::ostringstream buf; + + if(hist[0].shell.size() > 0 && first == 0){ + for (const auto& cmd : hist[0].shell){ + buf << '[' << cmd.execution_start << "] " << cmd.command << '\n' + << cmd.output << '\n' + << "[exit code: " << cmd.exit_code + << ", finished " << cmd.execution_end << "]\n\n"; + } + buf << "----------------------------------------\n"; + } + + const int total = static_cast(hist.size()); + const int firstIdx = std::max(1, first); + const int lastIdx = std::min(total, last + 1); + + if (lastIdx == 1 && hist[0].shell.empty()) { + std::cerr << "Nothing to bookmark – history is empty\n"; + return; + } + + if (firstIdx > lastIdx) { + std::cerr << "Invalid range (" << firstIdx << "-" << lastIdx << ")\n"; return; } - auto pair = agency_manager->session_history[len - index]; + for (int i = firstIdx; i < lastIdx; i++) + { + const auto& inter = hist[i]; + buf << '[' << inter.timestamp << "] Question: " << inter.question << '\n' + << "Answer: " << inter.answer << "\n\n"; + + for (const auto& cmd : inter.shell) + { + buf << '[' << cmd.execution_start << "] " << cmd.command << '\n' + << cmd.output << '\n' + << "[exit code: " << cmd.exit_code + << ", finished " << cmd.execution_end << "]\n\n"; + } + buf << "----------------------------------------\n"; + } + + std::string question = ""; + if(firstIdx != lastIdx){ + question = hist[firstIdx].question; + } + + bookmarks[alias] = std::make_pair( + /* title = */ question, + /* body = */ buf.str() + ); - // bookmark - bookmarks[alias] = pair; - std::cout << "Saved the query under the bookmark '" << alias << "'" << "\n"; + std::cout << "Bookmark added: " << alias << '\n'; } void BookmarkManager::remove_bookmark(const std::string &alias) { @@ -135,7 +184,7 @@ void BookmarkManager::remove_bookmark(const std::string &alias) { } void BookmarkManager::list_bookmarks() const { - constexpr int alias_width = 20; + constexpr int alias_width = 30; constexpr int query_width = 50; std::cout << std::left << std::setw(alias_width) << "BOOKMARK" << std::setw(query_width) << "QUERY" << "\n"; diff --git a/tui-tux/src/command_manager.cpp b/tui-tux/src/command_manager.cpp index 8cd3a8f..e1f66c0 100644 --- a/tui-tux/src/command_manager.cpp +++ b/tui-tux/src/command_manager.cpp @@ -2,12 +2,14 @@ #include #include #include +#include #define MANUALS_PATH_1 "/etc/ishell/manuals" #define MANUALS_PATH_2 "manuals" -CommandManager::CommandManager(BookmarkManager *bookmark_manager) { +CommandManager::CommandManager(BookmarkManager *bookmark_manager, MarkdownManager *markdown_manager) { this->bookmark_manager = bookmark_manager; + this->markdown_manager = markdown_manager; } void CommandManager::run_command(std::string &command) { @@ -33,13 +35,13 @@ void CommandManager::run_command(std::string &command) { void CommandManager::run_alias(std::string &alias) { const std::pair bookmark = bookmark_manager->get_bookmark(alias); - bookmark_manager->agency_manager->session_history.push_back(bookmark); + // bookmark_manager->agency_manager->session_history.push_back(bookmark); std::cout << bookmark.second << "\n"; } void CommandManager::clear(const std::vector &args) { if (args.empty()) { - bookmark_manager->agency_manager->session_history.clear(); + // bookmark_manager->agency_manager->session_history.clear(); std::cout << "Cleared session history.\n\n"; } } @@ -97,14 +99,32 @@ void CommandManager::bookmark(const std::vector &args) { } std::cerr << "Error: Could not find manual page 'bookmark.txt'.\n"; - } else if (!args.empty()) { - int index = 1; // default index if not provided + } else if (args.size() > 1 && is_range_token(args[0])) { + std::vector index; + int first = 0; + int last = 9999; int alias_begin_index = 0; if (args.size() > 1) { - try { - index = std::stoi(args[0]); - alias_begin_index++; - } catch ([[maybe_unused]] std::invalid_argument &e) {} + const std::string &range = args[0]; + const auto dash = range.find('-'); + + if (dash != std::string::npos) { + alias_begin_index = 1; + + std::string left = range.substr(0, dash); + std::string right = range.substr(dash + 1); + + try { + if (!left.empty()) + first = std::stoul(left); + + if (!right.empty()) + last = std::stoul(right); + } catch (const std::exception &) { + std::cerr << "Error: invalid range format\n"; + return; + } + } } auto split_alias = std::vector(args.begin() + alias_begin_index, args.end()); @@ -115,11 +135,83 @@ void CommandManager::bookmark(const std::vector &args) { return; } - bookmark_manager->bookmark(index, alias); + bookmark_manager->bookmark(first, last, alias); } else { std::cerr << "Error: Invalid bookmark command format. Try bookmark --help.\n"; } } - - +void CommandManager::markdown(const std::vector &args){ + if(args.size() > 1 && (args[0] == "-c" || args[0] == "--create")){ + auto split_alias = std::vector(args.begin() + 1, args.end()); + const std::string alias = join(split_alias, ' '); + if(!bookmark_manager->is_bookmark(alias)){ + std::cerr << "Error: Invalid markdown command format. bookmark: " + alias + " not found.\n"; + return; + } + + const std::pair bookmark = bookmark_manager->get_bookmark(alias); + const std::string llm_query = R"DELIM( + Analyze the provided history of my conversation with you and the commands I entered into the Linux terminal to solve my problem. + + Based on the information provided, create a full Markdown (.md) document containing: + + 1. A clear description of the problem I was trying to solve + 2. A step-by-step solution with detailed explanations of each step + 3. All necessary commands with comments on what they do + 4. Warnings about possible risks or side effects (if applicable) + 5. Alternative approaches to the solution (if any) + 6. Recommendations for preventing similar problems in the future + + The document format should be as readable as possible, with the correct heading structure, tables, and code formatting. + )DELIM" "\n" + bookmark.second; + + std::string summary_md = bookmark_manager->agency_manager->execute_query(bookmark_manager->agency_manager->get_agency_url() + "/assistant", llm_query, false); + markdown_manager->markdown(std::make_pair(alias, summary_md)); + } else if(args.size() > 0 && (args[0] == "-l" || args[0] == "--list")){ + markdown_manager->list_markdowns(); + } else if(args.size() > 1 && (args[0] == "-r" || args[0] == "--remove")){ + auto split_alias = std::vector(args.begin() + 1, args.end()); + if (const std::string alias = join(split_alias, ' '); markdown_manager->is_markdown(alias)) { + markdown_manager->remove_markdown(alias); + } + } else if(args.size() > 1 && (args[0] == "-s" || args[0] == "--save")){ + auto split_alias = std::vector(args.begin() + 1, args.end()); + if (const std::string alias = join(split_alias, ' '); markdown_manager->is_markdown(alias)) { + markdown_manager->save_as_file(alias); + } + } else if(args.size() > 1 && (args[0] == "-a" || args[0] == "--add")){ + auto group_start = std::find(args.begin(), args.end(), std::string("-g")); + if(group_start == args.end()){ + group_start = std::find(args.begin(), args.end(), std::string("--group")); + if(group_start == args.end()){ + std::cerr << "Error: Invalid markdown command format. Group to add not found.\n"; + return; + } + } + auto split_alias = std::vector(args.begin() + 1, group_start); + auto split_group = std::vector(group_start + 1, args.end()); + const std::string alias = join(split_alias, ' '); + if (!markdown_manager->is_markdown(alias)) { + std::cerr << "Error: Invalid markdown command format. Markdown for " + alias + " not found.\n"; + return; + } + markdown_manager->add_to_group(join(split_group, ' '), alias); + } else if(args.size() > 1 && args[0] == "-gpt"){ + auto split_group = std::vector(args.begin() + 1, args.end()); + std::vector markdown_list = markdown_manager->get_by_group(join(split_group, ' ')); + bookmark_manager->agency_manager->request_wrapper->context = markdown_list; + std::cout << "group: " + join(split_group, ' ') + " was adeed to agent context\n"; + } else if(args.size() > 0){ + auto split_alias = std::vector(args.begin(), args.end()); + const std::string alias = join(split_alias, ' '); + if(!markdown_manager->is_markdown(alias)){ + std::cerr << "Error: Invalid markdown command format. Markdown " + alias + " not found.\n"; + return; + } + auto md = markdown_manager->get_markdown(alias); + std::cout << md.second << "\n"; + } else{ + std::cerr << "Error: Invalid markdown command format.\n"; + } +} diff --git a/tui-tux/src/escape.cpp b/tui-tux/src/escape.cpp index 2e5f25a..704cafc 100644 --- a/tui-tux/src/escape.cpp +++ b/tui-tux/src/escape.cpp @@ -54,10 +54,32 @@ TerminalChar escape(const std::string &seq) { return ret; } +static TerminalChar make_osc_mark(const std::string& body) +{ + TerminalChar t{}; + + std::smatch m; + std::regex r(R"(333;([ABCD])(?:;(\d+))?)"); + if (!std::regex_match(body, m, r)) return t; + + char id = m[1].str()[0]; + switch(id) { + case 'A': t.ch = E_OSC_PROMPT_START; break; + case 'B': t.ch = E_OSC_PROMPT_END; break; + case 'C': t.ch = E_OSC_PRE_EXEC; break; + case 'D': t.ch = E_OSC_CMD_FINISH; + if (m[2].matched) t.args.push_back(std::stoi(m[2])); + break; + } + return t; +} + int read_and_escape(const int fd, std::vector &vec) { struct FdEscapeData { bool in_escape{}; + bool in_osc{}; std::string escape_seq; + std::string osc_seq; }; static std::unordered_map fd_escape_data; @@ -77,14 +99,36 @@ int read_and_escape(const int fd, std::vector &vec) { vec = std::vector(); for (ssize_t i = 0; i < n; i++) { - // ESC sequence + /* ── начало ESC (CSI или OSC) ── */ if (buf[i] == 0x1B) { fd_escape_data[fd].in_escape = true; + if (i+1 < n && buf[i+1] == ']') { // OSC + fd_escape_data[fd].in_osc = true; + fd_escape_data[fd].osc_seq = ""; + i++; // пропустили ']' + continue; + } fd_escape_data[fd].escape_seq = "\x1B"; continue; } if (fd_escape_data[fd].in_escape) { + /* ────────── внутри OSC ────────── */ + if (fd_escape_data[fd].in_osc) { + // terminator: BEL 0x07 ИЛИ ESC + if (buf[i] == 0x07 || (buf[i] == 0x1B && i+1 < n && buf[i+1]=='\\')) { + TerminalChar tch = make_osc_mark(fd_escape_data[fd].osc_seq); + if (tch.ch) vec.push_back(tch); + + fd_escape_data[fd].in_osc = false; + fd_escape_data[fd].in_escape = false; + fd_escape_data[fd].osc_seq.clear(); + continue; + } + fd_escape_data[fd].osc_seq.push_back(buf[i]); + continue; + } + if (buf[i] != 0x9c) { fd_escape_data[fd].escape_seq += buf[i]; } diff --git a/tui-tux/src/markdown_manager.cpp b/tui-tux/src/markdown_manager.cpp new file mode 100644 index 0000000..2efff48 --- /dev/null +++ b/tui-tux/src/markdown_manager.cpp @@ -0,0 +1,134 @@ +#include "markdown_manager.hpp" +#include +#include + +MarkdownManager::MarkdownManager() = default; + +MarkdownManager::~MarkdownManager() = default; + +bool MarkdownManager::create_markdowns_file(const std::string& filename) { + std::ofstream create_file(filename); + if (!create_file) { + std::cerr << "Error creating file: " << filename << "\n"; + return false; + } + const json empty_markdowns = json::array(); + create_file << empty_markdowns.dump(4); + return true; +} + +void MarkdownManager::parse_markdown_json(const json &markdown) { + const auto bookmark = markdown.at("bookmark").get(); + const auto group = markdown.at("group").get(); + const auto protocol = markdown.at("protocol").get(); + markdowns[bookmark] = {group, protocol}; +} + +void MarkdownManager::load_markdowns(const std::string &filename) { + std::ifstream file(filename); + if (!file) { + if (!create_markdowns_file(filename)) { + return; + } + return; + } + json markdown_json; + file >> markdown_json; + for (const json &markdown : markdown_json) { + parse_markdown_json(markdown); + } +} + +void MarkdownManager::save_markdowns(const std::string &filename) { + std::ofstream file(filename); + if (!file.is_open()) { + std::cerr << "Error opening file: " << filename << "\n"; + return; + } + json markdown_json = json::array(); + for (const auto &markdown : markdowns) { + json entry; + entry["bookmark"] = markdown.first; + entry["group"] = markdown.second.first; + entry["protocol"] = markdown.second.second; + markdown_json.push_back(entry); // Add entry to the array + } + file << markdown_json.dump(4); +} + +bool MarkdownManager::is_markdown(const std::string& n) const { + return markdowns.find(n) != markdowns.end(); +} + +std::pair MarkdownManager::get_markdown(const std::string& n) const { + auto it = markdowns.find(n); + return it == markdowns.end() ? std::make_pair(std::string(), std::string()) : it->second; +} + +/*---------------------------------------------------------------------------------------*/ + +void MarkdownManager::markdown(const std::pair& protocol) { + markdowns[protocol.first] = std::make_pair( "default", protocol.second); + std::cout << "Markdown added: " << protocol.first << '\n'; +} + +void MarkdownManager::list_markdowns() const { + constexpr int group_width = 20; + constexpr int bookmark_width = 50; + std::cout << std::left << std::setw(bookmark_width) << "MARKDOWN" + << std::setw(group_width) << "GROUP" << "\n"; + for (const auto &[fst, snd] : markdowns) { + std::cout << std::left << std::setw(bookmark_width) << fst + << std::setw(group_width) << snd.first + << "\n"; + } +} + + +void MarkdownManager::remove_markdown(const std::string &bookmark) { + if (const auto it = markdowns.find(bookmark); it != markdowns.end()) { + markdowns.erase(it); + std::cout << "Removed markdown '" << bookmark << "'.\n"; + } else { + std::cerr << "Error: Markdown '" << bookmark << "' not found.\n"; + + } +} + +void MarkdownManager::add_to_group(const std::string& group, + const std::string& bookmark) { + auto it = markdowns.find(bookmark); + if (it == markdowns.end()) { + std::cerr << "Error: Markdown '" << bookmark << "' not found.\n"; + return; + } + it->second.first = group; + //save_markdowns(get_markdowns_file_path()); + std::cout << "Markdown '" << bookmark << "' moved to '" << group << "'.\n"; +} + +std::vector MarkdownManager::get_by_group(const std::string& group) { + std::vector context; + for (auto& [_, md] : markdowns){ + if (md.first == group) { + context.push_back(md.second); + } + } + return context; +} + +bool MarkdownManager::save_as_file(const std::string& bookmark) { + auto it = markdowns.find(bookmark); + if (it == markdowns.end()) { + std::cerr << "Error: Markdown '" << bookmark << "' not found.\n"; + return false; + } + std::ofstream f(bookmark + ".md"); + if (!f) { + std::cerr << "Error creating file: " << bookmark << ".md\n"; + return false; + } + f << it->second.second << '\n'; + std::cout << "Saved markdown '" << bookmark << "' to " << bookmark << ".md\n"; + return true; +} diff --git a/tui-tux/src/screen.cpp b/tui-tux/src/screen.cpp index 83764ee..186e896 100644 --- a/tui-tux/src/screen.cpp +++ b/tui-tux/src/screen.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -497,4 +498,18 @@ int Screen::wmove(WINDOW *window, int y, int x) { } return rc; +} + +std::string Screen::get_line(int line_num) { + char buffer[512]; + memset(buffer, 0, sizeof(buffer)); + + int cur_y, cur_x; + getyx(pad, cur_y, cur_x); + + mvwinnstr(pad, line_num, 0, buffer, sizeof(buffer) - 1); + + wmove(pad, cur_y, cur_x); + + return std::string(buffer); } \ No newline at end of file diff --git a/tui-tux/src/session_tracker.cpp b/tui-tux/src/session_tracker.cpp new file mode 100644 index 0000000..d57f0f2 --- /dev/null +++ b/tui-tux/src/session_tracker.cpp @@ -0,0 +1,258 @@ +#include "session_tracker.hpp" +#include +#include +#include +#include +#include +#include +#include + +SessionTracker& SessionTracker::get() { + static SessionTracker instance; + return instance; +} + +SessionTracker::SessionTracker() + : db(nullptr), sessionDbId(-1), lastCommandId(-1) { + openLogFile(); +} + +SessionTracker::~SessionTracker() { + endSession(); +} + +void SessionTracker::startSession() { + if (!db) return; + + const char *insertSession = "INSERT INTO sessions DEFAULT VALUES;"; + if (sqlite3_exec(db, insertSession, nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "Failed to insert session: " << sqlite3_errmsg(db) << std::endl; + sessionDbId = -1; + } else { + sessionDbId = sqlite3_last_insert_rowid(db); + } + + lastCommandId = -1; + + this->logAgentInteraction("Hello!", "Hello! How can I assist you with your Linux shell commands today? How can I assist you today?"); +} + +void SessionTracker::endSession() { + if (!db || sessionDbId == -1) return; + + std::string sql = "UPDATE sessions SET end_time = CURRENT_TIMESTAMP WHERE session_id = " + std::to_string(sessionDbId) + ";"; + if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "Failed to update session: " << sqlite3_errmsg(db) << std::endl; + } + + sqlite3_close(db); + db = nullptr; + sessionDbId = -1; + lastCommandId = -1; +} + +void SessionTracker::openLogFile() { + const std::string dbPath = "./local/session_history.db"; + if (sqlite3_open(dbPath.c_str(), &db) != SQLITE_OK) { + std::cerr << "Failed to open SQLite database: " << sqlite3_errmsg(db) << std::endl; + db = nullptr; + return; + } + + std::string schema; + try { + std::ifstream inFile("./schema.sql"); + if (!inFile) { + throw std::runtime_error("Can't open file: ./schema.sql"); + } + std::stringstream buffer; + buffer << inFile.rdbuf(); + schema = buffer.str(); + } catch (const std::exception &ex) { + std::cerr << ex.what() << std::endl; + return; + } + + if (sqlite3_exec(db, schema.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "Failed to create schema: " << sqlite3_errmsg(db) << std::endl; + } +} + +//---------------------------------- tracking user interaction --------------------------------------- + +void SessionTracker::logAgentInteraction(const std::string& question, const std::string& response) { + if (!db || sessionDbId == -1) return; + + char *escapedQuestion = sqlite3_mprintf("%q", question.c_str()); + char *escapedResponse = sqlite3_mprintf("%q", response.c_str()); + + std::string sql = "INSERT INTO interactions (session_id, agent_question, agent_message) VALUES (" + + std::to_string(sessionDbId) + ", '" + escapedQuestion + "', '" + escapedResponse + "');"; + + if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "DB insert interaction failed: " << sqlite3_errmsg(db) << std::endl; + } + + sqlite3_free(escapedQuestion); + sqlite3_free(escapedResponse); +} + +void SessionTracker::addNewCommand(EventType command_type) { + if (!db) return; + + bool is_shell_command = (command_type == EventType::ShellCommand); + + auto callback = [](void *data, int argc, char **argv, char **) -> int { + int *pmax = static_cast(data); + *pmax = argv[0] ? std::atoi(argv[0]) : 0; + return 0; + }; + + int last_interaction_id = 0; + std::string sql = "SELECT MAX(interaction_id) AS max_id FROM interactions;"; + if (sqlite3_exec(db, sql.c_str(), callback, &last_interaction_id, nullptr) != SQLITE_OK) { + std::cerr << "Error selecting max interaction_id\n"; + } + + sql = "INSERT INTO commands (interaction_id, command_text, shell_command) VALUES (" + + (last_interaction_id > 0 ? std::to_string(last_interaction_id) : "NULL") + + ", '', " + + (is_shell_command ? "1" : "0") + + ");"; + + if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "Failed to insert command: " << sqlite3_errmsg(db) << std::endl; + lastCommandId = -1; + } else { + lastCommandId = sqlite3_last_insert_rowid(db); + } +} + +void SessionTracker::appendCommandText(const std::string& text) { + if (!db || lastCommandId == -1) return; + + char *escapedText = sqlite3_mprintf("%q", text.c_str()); + + std::string sql = "UPDATE commands SET command_text = '" + std::string(escapedText) + + "' WHERE command_id = " + std::to_string(lastCommandId) + ";"; + + if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "Failed to update command text: " << sqlite3_errmsg(db) << std::endl; + } + + sqlite3_free(escapedText); +} + +void SessionTracker::setCommandOutput(const std::string& output) { + if (!db || lastCommandId == -1) return; + + char *escapedOutput = sqlite3_mprintf("%q", output.c_str()); + + std::string sql = "UPDATE commands SET output = '" + std::string(escapedOutput) + + "', execution_end = CURRENT_TIMESTAMP WHERE command_id = " + + std::to_string(lastCommandId) + ";"; + + if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "Failed to update command output: " << sqlite3_errmsg(db) << std::endl; + } + + sqlite3_free(escapedOutput); +} + +void SessionTracker::setExitCode(int exit_code) { + if (!db || lastCommandId == -1) return; + + std::string sql = "UPDATE commands SET exit_code = " + std::to_string(exit_code) + + " WHERE command_id = " + std::to_string(lastCommandId) + ";"; + + if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { + std::cerr << "Failed to update command exit code: " << sqlite3_errmsg(db) << std::endl; + } +} + +//----------------------------------------- get methods ------------------------------------------ + +const std::vector& SessionTracker::get_history() const { + history.clear(); + if (!db || sessionDbId <= 0) + return history; + + std::unordered_map rowOf; + char* err = nullptr; + + const std::string sqlInts = + "SELECT interaction_id, timestamp, agent_question, agent_message " + "FROM interactions " + "WHERE session_id = " + std::to_string(sessionDbId) + " " + "ORDER BY interaction_id ASC;"; + + auto intsCB = [](void* ctx, int n, char** row, char**) -> int + { + using Pair = std::pair*, std::unordered_map*>; + auto* p = static_cast(ctx); + auto& vec = *p->first; + auto& map = *p->second; + if (n < 4) return 0; + + Interaction inter; + inter.interaction_id = std::atoi(row[0]); + inter.timestamp = row[1] ? row[1] : ""; + inter.question = row[2] ? row[2] : ""; + inter.answer = row[3] ? row[3] : ""; + + map[inter.interaction_id] = vec.size(); + vec.push_back(std::move(inter)); + return 0; + }; + + std::pair*, std::unordered_map*> ctxInts{&history, &rowOf}; + if (sqlite3_exec(db, sqlInts.c_str(), intsCB, &ctxInts, &err) != SQLITE_OK) + { + if (err) sqlite3_free(err); + return history; + } + + if (history.empty()) + return history; + + // shell commands + const std::string sqlCmds = + "SELECT interaction_id, command_text, output, " + " execution_start, execution_end, exit_code " + "FROM commands " + "WHERE shell_command = 1 " + " AND interaction_id IN (" + " SELECT interaction_id " + " FROM interactions " + " WHERE session_id = " + std::to_string(sessionDbId) + ") " + "ORDER BY interaction_id ASC, command_id ASC;"; + + auto cmdsCB = [](void* ctx, int n, char** row, char**) -> int + { + using Pair = std::pair*, std::unordered_map*>; + auto* p = static_cast(ctx); + auto& vec = *p->first; + auto& map = *p->second; + if (n < 6) return 0; + + const int id = std::atoi(row[0]); + auto pos = map.find(id); + if (pos == map.end()) return 0; + + ShellCmd cmd; + cmd.interaction_id = id; + cmd.command = row[1] ? row[1] : ""; + cmd.output = row[2] ? row[2] : ""; + cmd.execution_start = row[3] ? row[3] : ""; + cmd.execution_end = row[4] ? row[4] : ""; + cmd.exit_code = row[5] ? std::atoi(row[5]) : 0; + + vec[pos->second].shell.push_back(std::move(cmd)); + return 0; + }; + + sqlite3_exec(db, sqlCmds.c_str(), cmdsCB, &ctxInts, &err); + if (err) sqlite3_free(err); + + return history; +} \ No newline at end of file diff --git a/tui-tux/src/terminal_multiplexer.cpp b/tui-tux/src/terminal_multiplexer.cpp index 336ee6c..320b8ad 100644 --- a/tui-tux/src/terminal_multiplexer.cpp +++ b/tui-tux/src/terminal_multiplexer.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #define MAGENTA_FOREGROUND 1 @@ -47,8 +48,8 @@ void TerminalMultiplexer::init() { if (bash_pid < 0) { perror("fork: bash_pty"); exit(EXIT_FAILURE); - } - + } + if (bash_pid == 0) { // Child process will execute the shell close(pty_bash_master); @@ -69,7 +70,7 @@ void TerminalMultiplexer::init() { close(pty_bash_slave); // Set TERM type - setenv("TERM", "ishell-m", 1); + setenv("TERM", "ishell-m", 1); // xterm-256color // Execute the shell execl(shell, shell, NULL); @@ -166,6 +167,7 @@ void TerminalMultiplexer::init_nc() { noecho(); create_wins_draw(); + send_dims(); } void TerminalMultiplexer::refresh_cursor() const { @@ -268,7 +270,7 @@ void TerminalMultiplexer::create_wins_draw() { // Resize old screens std::vector new_screens; - + // Agent Screen screen = Screen(agent_lines, agent_cols, screens[0]); screen.set_screen_coords(agent_y, agent_x, agent_y + agent_lines - 1, agent_x + agent_cols - 1); @@ -434,7 +436,7 @@ void TerminalMultiplexer::run_terminal() { if (n_in == 0) { epolling = false; break; - } + } } else if (events[i].data.fd == sigfd) { // Read the signal signalfd_siginfo sigfd_info{}; @@ -474,7 +476,7 @@ void TerminalMultiplexer::run_terminal() { close(epoll_fd); } -int TerminalMultiplexer::handle_screen_output(Screen &screen, const int fd) const { +int TerminalMultiplexer::handle_screen_output(Screen &screen, const int fd) { int bytes_read = 0; while (true) { @@ -499,9 +501,72 @@ int TerminalMultiplexer::handle_screen_output(Screen &screen, const int fd) cons exit(EXIT_FAILURE); } + //-------------------------------------------------- прочитали символы которые надо передать на экран (как пользователя так и самого окнка) ----------------------------- if (n > 0) { bytes_read += n; for (TerminalChar &tch : chars) { + /* ────── Bash‑специфичные метки OSC 133 ───── */ + bool is_bash_screen = (&screen == &screens[FOCUS_BASH]); + if (is_bash_screen && tch.ch >= E_OSC_PROMPT_START && tch.ch <= E_OSC_CMD_FINISH) { + switch (tch.ch) { + case E_OSC_PROMPT_START: { + bash_waiting_for_prompt = false; + int command_line = getcury(screen.get_pad()); + if(last_command_line >= 0 && command_line == (last_command_line + 1)) { + last_command_line = command_line; + }else{ + first_command_line = command_line; + last_command_line = command_line; + } + break; + } + + case E_OSC_PRE_EXEC: { + if(bash_waiting_for_prompt) break; + std::string cmd = ""; + for (int i = first_command_line; i <= last_command_line; i++) { + std::string line = screen.get_line(i); + auto endpos = line.find_last_not_of(' '); + if (endpos != std::string::npos) { + line = line.substr(0, endpos + 1); + } else { + line.clear(); + } + cmd += line; + if(i != last_command_line){ + cmd += "\n"; + } + } + SessionTracker::get().addNewCommand(SessionTracker::EventType::ShellCommand); + SessionTracker::get().appendCommandText(cmd); + + bash_capturing = true; + break; + } + + case E_OSC_CMD_FINISH: { + if(bash_waiting_for_prompt) break; + bash_capturing = false; + + std::string cmd = ""; + int command_line = getcury(screen.get_pad()); + for (int i = last_command_line + 1; i <= command_line; i++) { + cmd += screen.get_line(i); + cmd += "\n"; + } + SessionTracker::get().setCommandOutput(cmd); + if (!tch.args.empty()) + SessionTracker::get().setExitCode(tch.args[0]); + + first_command_line = -10; + last_command_line = -10; + bash_waiting_for_prompt = true; + break; + } + } + continue; + } + screen.handle_char(tch); } } @@ -560,6 +625,7 @@ int TerminalMultiplexer::handle_input() { refresh_cursor(); } } else { + // ----------------------------------------------------- читаем символы введеные пользователем ----------------------------------------------- for (const char ch1 : tch.sequence) { handle_pty_input(screens[focus].get_pty_master(), ch1); } diff --git a/tui-tux/src/utils.cpp b/tui-tux/src/utils.cpp index de26631..0d889e2 100644 --- a/tui-tux/src/utils.cpp +++ b/tui-tux/src/utils.cpp @@ -1,6 +1,8 @@ #include -std::vector split(std::string &str, char delim, bool ignore_empty) { +#include + +std::vector split(const std::string &str, char delim, bool ignore_empty) { std::vector words; std::string current_str; @@ -38,3 +40,10 @@ std::string join(std::vector &words, const char delim) { return str; } + +bool is_range_token(const std::string& s) { + // "N-M", "-M", "N-" + static const std::regex re(R"(^(\d*)-(\d*)$)"); + std::smatch m; + return std::regex_match(s, m, re) && (m[1].length() || m[2].length()); +} diff --git a/tui-tux/test/test_agency_manager.cpp b/tui-tux/test/test_agency_manager.cpp index ebef704..e6c31ae 100644 --- a/tui-tux/test/test_agency_manager.cpp +++ b/tui-tux/test/test_agency_manager.cpp @@ -94,7 +94,7 @@ TEST_F(AgencyManagerTest, ExecuteQuery_AskAgentFails) { const std::string url = agency_manager.get_agency_url(); EXPECT_CALL(mock_request_wrapper, ask_agent("http://localhost:5000/assistant", "test query", agency_manager.session_history)) .WillOnce(::testing::Return("")); - // act + // act. const std::string result = agency_manager.execute_query(url + "/assistant", "test query"); // assert EXPECT_EQ(result, "");