Branched WIP that needs to be merged with the new changes from develop.

This commit is contained in:
Martín Lucas Golini
2026-03-21 15:07:32 -03:00
parent bbba9b9023
commit 03acb3f95b
9 changed files with 318 additions and 48 deletions

View File

@@ -216,6 +216,16 @@ void ACPClient::newSession( const NewSessionRequest& req,
} );
}
void ACPClient::loadSession( const LoadSessionRequest& req,
const std::function<void( const LoadSessionResponse& )>& cb ) {
write( { { "method", "session/load" }, { "params", req.toJson() } },
[cb]( const IdType&, const json& resp ) {
if ( resp.contains( "result" ) && cb ) {
cb( LoadSessionResponse( resp["result"] ) );
}
} );
}
void ACPClient::prompt( const PromptRequest& req,
const std::function<void( const PromptResponse& )>& cb ) {
write( { { "method", "session/prompt" }, { "params", req.toJson() } },

View File

@@ -47,6 +47,8 @@ class ACPClient {
const std::function<void( const InitializeResponse& )>& cb );
void newSession( const NewSessionRequest& req,
const std::function<void( const NewSessionResponse& )>& cb );
void loadSession( const LoadSessionRequest& req,
const std::function<void( const LoadSessionResponse& )>& cb );
void prompt( const PromptRequest& req, const std::function<void( const PromptResponse& )>& cb );
// Notifications to agent

View File

@@ -60,6 +60,21 @@ NewSessionResponse::NewSessionResponse( const json& body ) {
configOptions = body["configOptions"];
}
json LoadSessionRequest::toJson() const {
json j = { { "sessionId", sessionId }, { "cwd", cwd } };
if ( mcpServers.is_null() ) {
j["mcpServers"] = json::array();
} else {
j["mcpServers"] = mcpServers;
}
return j;
}
LoadSessionResponse::LoadSessionResponse( const json& body ) {
if ( body.contains( "configOptions" ) )
configOptions = body["configOptions"];
}
json PromptRequest::toJson() const {
return { { "sessionId", sessionId }, { "prompt", prompt } };
}

View File

@@ -63,6 +63,22 @@ struct NewSessionResponse {
NewSessionResponse( const json& body );
};
struct LoadSessionRequest {
std::string sessionId;
std::string cwd;
json mcpServers;
LoadSessionRequest() = default;
json toJson() const;
};
struct LoadSessionResponse {
json configOptions;
LoadSessionResponse() = default;
LoadSessionResponse( const json& body );
};
struct PromptRequest {
std::string sessionId;
json prompt; // Array of ContentBlock

View File

@@ -37,6 +37,38 @@ bool AgentSession::start( const std::function<void( bool )>& onReady ) {
return false;
}
bool AgentSession::startLoaded( const std::string& sessionId,
const std::function<void( bool )>& onReady ) {
if ( mClient->start() ) {
InitializeRequest req;
req.clientCapabilities.terminal = true;
req.clientCapabilities.fsReadTextFile = true;
req.clientCapabilities.fsWriteTextFile = true;
mClient->initialize( req, [this, sessionId, onReady]( const InitializeResponse& ires ) {
if ( ires.agentCapabilities.loadSession ) {
LoadSessionRequest lreq;
lreq.sessionId = sessionId;
lreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : "";
mClient->loadSession( lreq, [this, sessionId, onReady]( const LoadSessionResponse& ) {
mSessionId = sessionId;
if ( onReady )
onReady( true );
} );
} else {
// Agent doesn't support loading, fallback to new session?
// For now let's just fail or call onReady(false)
if ( onReady )
onReady( false );
}
} );
return true;
}
if ( onReady )
onReady( false );
return false;
}
void AgentSession::stop() {
if ( mClient )
mClient->stop();
@@ -58,6 +90,11 @@ void AgentSession::cancel() {
}
}
void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* uiTerm ) {
mTerminals[terminalId] =
TermData{ uiTerm->getTerm(), uiTerm->getTerm()->getTerminal(), uiTerm };
}
void AgentSession::setupClient() {
mClient->onSessionUpdate = [this]( const json& msg ) {
if ( onSessionUpdate )
@@ -92,10 +129,8 @@ void AgentSession::setupClient() {
CreateTerminalResponse res;
std::string termId = String::format( "term-%u", String::hash( req.command ) );
res.terminalId = termId;
// Wait for UI? No, ACPClient is running in threads. We just trigger the event
// and return the ID.
if ( onTerminalCreated ) {
onTerminalCreated( nullptr, termId );
onTerminalCreated( req, termId );
}
cb( res );
};

View File

@@ -21,6 +21,7 @@ class AgentSession {
~AgentSession();
bool start( const std::function<void( bool )>& onReady );
bool startLoaded( const std::string& sessionId, const std::function<void( bool )>& onReady );
void stop();
void prompt( const PromptRequest& req, const std::function<void( const PromptResponse& )>& cb );
@@ -32,11 +33,14 @@ class AgentSession {
std::function<void( const RequestPermissionRequest&,
std::function<void( const RequestPermissionResponse& )> )>
onRequestPermission;
std::function<void( UITerminal*, const std::string& terminalId )> onTerminalCreated;
std::function<void( const CreateTerminalRequest&, const std::string& terminalId )>
onTerminalCreated;
std::string getSessionId() const { return mSessionId; }
ACPClient* getClient() const { return mClient.get(); }
void setTerminalData( const std::string& terminalId, UITerminal* uiTerm );
protected:
std::shared_ptr<ThreadPool> mThreadPool;
std::unique_ptr<ACPClient> mClient;

View File

@@ -21,6 +21,7 @@
#include <eepp/ui/uitooltip.hpp>
#include <eepp/window/clipboard.hpp>
#include <eepp/window/window.hpp>
#include <eterm/ui/uiterminal.hpp>
#include <chrono>
#include <nlohmann/json.hpp>
@@ -239,8 +240,7 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
mChatAgentMode = find<UISelectButton>( "llm_agent_mode" );
mChatAgentMode->on( Event::OnValueChange, [this]( auto ) {
mIsAgentMode = mChatAgentMode->isSelected();
mModelDDL->setVisible( !mIsAgentMode );
mAgentDDL->setVisible( mIsAgentMode );
updateAgentModeUI();
} );
// mRefreshModels = find<UIPushButton>( "refresh_model_ui" );
@@ -649,6 +649,7 @@ LLMChatUI::~LLMChatUI() {
config.partition = getSplitter()->getSplitPartition();
config.modelProvider = mCurModel.provider;
config.modelName = mCurModel.name;
config.agentName = mCurAgent;
getPlugin()->setConfig( std::move( config ) );
}
}
@@ -1025,7 +1026,15 @@ void LLMChatUI::fillAgentDropDownList( UIDropDownList* agentDDL ) {
agentDDL->getListBox()->addListBoxItems( std::move( agents ) );
agentDDL->getListBox()->setSelected( selectedIndex );
agentDDL->on( Event::OnValueChange, [this, agentDDL]( auto ) {
mCurAgent = agentDDL->getListBox()->getItemSelectedText().toUtf8();
auto newAgent = agentDDL->getListBox()->getItemSelectedText().toUtf8();
if ( newAgent != mCurAgent ) {
mCurAgent = newAgent;
if ( mAgentSession ) {
mAgentSession->stop();
mAgentSession.reset();
}
updateTabTitle();
}
} );
}
@@ -1047,6 +1056,106 @@ void LLMChatUI::writeToLastChat( const std::string& text ) {
} );
}
void LLMChatUI::updateAgentModeUI() {
mModelDDL->setVisible( !mIsAgentMode );
mAgentDDL->setVisible( mIsAgentMode );
mChatAdd->setVisible( !mIsAgentMode );
mChatUserRole->setVisible( !mIsAgentMode );
auto chats = mChatsList->findAllByClass( "llm_conversation" );
for ( auto chat : chats ) {
if ( auto roleUi = chat->findByClass( "role_ui" ) )
roleUi->setVisible( !mIsAgentMode );
if ( auto moveUp = chat->findByClass( "move_up" ) )
moveUp->setVisible( !mIsAgentMode );
if ( auto moveDown = chat->findByClass( "move_down" ) )
moveDown->setVisible( !mIsAgentMode );
if ( auto eraseBut = chat->findByClass( "erase_but" ) )
eraseBut->setVisible( !mIsAgentMode );
}
}
void LLMChatUI::setupAgentSession() {
auto it = mAgents.find( mCurAgent );
if ( it == mAgents.end() )
return;
acp::ACPClient::Config config;
config.command = it->second.command;
config.args = it->second.args;
config.environment = it->second.environment;
config.workingDirectory = getPlugin()->getPluginContext()->getCurrentProject();
mAgentSession =
std::make_unique<acp::AgentSession>( getUISceneNode()->getThreadPool(), config );
mAgentSession->onSessionUpdate = [this]( const nlohmann::json& msg ) {
auto sessionUpdate = msg.value( "sessionUpdate", "" );
if ( sessionUpdate == "agent_message_chunk" || sessionUpdate == "agent_thought_chunk" ) {
if ( msg.contains( "content" ) && msg["content"].contains( "text" ) ) {
auto chunk = msg["content"].value( "text", "" );
writeToLastChat( chunk );
}
} else if ( sessionUpdate == "tool_call" ) {
std::string toolStr = "\n> Tool Call: " + msg.value( "title", "" ) + "\n";
writeToLastChat( toolStr );
} else if ( sessionUpdate == "plan" ) {
std::string planStr = "\n> Plan Updated:\n";
if ( msg.contains( "plan" ) && msg["plan"].contains( "steps" ) &&
msg["plan"]["steps"].is_array() ) {
for ( const auto& step : msg["plan"]["steps"] ) {
planStr += "- [" + step.value( "status", "" ) + "] " +
step.value( "title", "" ) + "\n";
}
}
writeToLastChat( planStr );
} else if ( sessionUpdate == "available_commands_update" ) {
if ( msg.contains( "availableCommands" ) && msg["availableCommands"].is_array() ) {
mAvailableCommands.clear();
for ( const auto& cmd : msg["availableCommands"] ) {
mAvailableCommands.push_back(
{ cmd.value( "name", "" ), cmd.value( "description", "" ) } );
}
}
}
};
mAgentSession->onRequestPermission = [this]( const auto& req, auto cb ) {
runOnMainThread( [this, req, cb]() { addPermissionUI( req, cb ); } );
};
mAgentSession->onTerminalCreated = [this]( const acp::CreateTerminalRequest& req,
const std::string& termId ) {
runOnMainThread( [this, req, termId] {
find( "chat_presentation" )->setVisible( false );
UIWidget* bubble = mChatsList->getUISceneNode()->loadLayoutFromString(
R"xml(<vbox class="llm_conversation terminal_bubble" lw="mp" lh="300dp" padding="8dp" background-color="var(--tab-back)" />)xml",
mChatsList );
std::unordered_map<std::string, std::string> env;
for ( const auto& e : req.env )
env[e.name] = e.value;
auto* uiTerm = eterm::UI::UITerminal::New(
getPlugin()->getPluginContext()->getTerminalFont(),
getPlugin()
->getPluginContext()
->termConfig()
.fontSize.asPixels(
getUISceneNode()->getPixelsSize().getWidth(),
getUISceneNode()->getPixelsSize(), getUISceneNode()->getDPI(),
getUISceneNode()->getUIThemeManager()->getDefaultFontSize() ),
Sizef( 0, 0 ), req.command, req.args, env,
req.cwd ? *req.cwd : getPlugin()->getPluginContext()->getCurrentProject(), 10000,
nullptr, false, false );
uiTerm->setParent( bubble );
uiTerm->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
mAgentSession->setTerminalData( termId, uiTerm );
} );
};
}
void LLMChatUI::doAgentRequest() {
if ( !mAgentSession ) {
auto it = mAgents.find( mCurAgent );
@@ -1058,14 +1167,7 @@ void LLMChatUI::doAgentRequest() {
mChatRun->setVisible( false )->setEnabled( false );
mChatStop->setVisible( true )->setEnabled( true );
acp::ACPClient::Config config;
config.command = it->second.command;
config.args = it->second.args;
config.environment = it->second.environment;
config.workingDirectory = getPlugin()->getPluginContext()->getCurrentProject();
mAgentSession =
std::make_unique<acp::AgentSession>( getUISceneNode()->getThreadPool(), config );
setupAgentSession();
UIWidget* chat = addChatUI( LLMChat::Role::Assistant );
toggleEnableChats( false );
@@ -1078,31 +1180,6 @@ void LLMChatUI::doAgentRequest() {
thinking->setInterval( [thinking] { thinking->rotate( 360 / 32 ); }, Seconds( 0.125 ),
thinkingID );
mAgentSession->onSessionUpdate = [this]( const nlohmann::json& msg ) {
auto sessionUpdate = msg.value( "sessionUpdate", "" );
if ( sessionUpdate == "agent_message_chunk" ||
sessionUpdate == "agent_thought_chunk" ) {
if ( msg.contains( "content" ) && msg["content"].contains( "text" ) ) {
auto chunk = msg["content"].value( "text", "" );
writeToLastChat( chunk );
}
} else if ( sessionUpdate == "tool_call" ) {
std::string toolStr = "\n> Tool Call: " + msg.value( "title", "" ) + "\n";
writeToLastChat( toolStr );
} else if ( sessionUpdate == "plan" ) {
std::string planStr = "\n> Plan Updated:\n";
writeToLastChat( planStr );
}
};
mAgentSession->onRequestPermission = [this]( const auto& req, auto cb ) {
runOnMainThread( [this, req, cb]() { addPermissionUI( req, cb ); } );
};
mAgentSession->onTerminalCreated = []( UITerminal* term, const std::string& termId ) {
// This will be called from AgentSession.
};
mAgentSession->start( [this]( bool ready ) {
runOnMainThread( [this, ready]() {
if ( ready ) {
@@ -1156,8 +1233,30 @@ void LLMChatUI::sendAgentPrompt() {
chats[chats.size() - 2]; // Assistant is the last one (added in doAgentRequest)
auto* editor = lastChat->findByClass<UICodeEditor>( "data_ui" );
std::string text = editor->getDocument().getText().toUtf8();
replaceFileLinksToContents( text );
req.prompt = { { { "type", "text" }, { "text", text } } };
bool isSlashCommand = false;
if ( String::startsWith( text, "/" ) ) {
auto spacePos = text.find( ' ' );
std::string cmdName = text.substr( 1, spacePos == std::string::npos ? std::string::npos
: spacePos - 1 );
std::string arg = spacePos == std::string::npos ? "" : text.substr( spacePos + 1 );
auto it = std::find_if( mAvailableCommands.begin(), mAvailableCommands.end(),
[&cmdName]( const SlashCommand& c ) {
return c.name == cmdName;
} );
if ( it != mAvailableCommands.end() ) {
req.prompt = { { { "type", "slash_command" },
{ "name", cmdName },
{ "argument", arg } } };
isSlashCommand = true;
}
}
if ( !isSlashCommand ) {
req.prompt = promptToContentBlocks( text );
}
} else {
req.prompt = { { { "type", "text" }, { "text", "" } } };
}
@@ -1166,7 +1265,7 @@ void LLMChatUI::sendAgentPrompt() {
runOnMainThread( [this, res]() {
auto chats = mChatsList->findAllByClass( "llm_conversation" );
if ( !chats.empty() ) {
auto* chat = chats[chats.size() - 1];
auto* chat = chats.back();
auto* editor = chat->findByClass<UICodeEditor>( "data_ui" );
auto* thinking = editor->findByClass<UIImage>( "thinking" );
if ( thinking ) {
@@ -1184,7 +1283,11 @@ void LLMChatUI::sendAgentPrompt() {
mChatRun->setVisible( true )->setEnabled( true );
if ( res.stopReason != "cancelled" ) {
saveChat();
if ( !mChatIsPrivate && !mSummaryRequest && mSummary.empty() ) {
generateChatName( false );
} else {
saveChat();
}
}
} );
} );
@@ -1229,6 +1332,44 @@ void LLMChatUI::replaceFileLinksToContents( std::string& text ) {
}
}
nlohmann::json LLMChatUI::promptToContentBlocks( std::string text ) {
auto j = nlohmann::json::array();
LuaPattern ptrn( "\n```file://([^`]*)```\n?" );
PatternMatcher::Range matches[2];
size_t lastPos = 0;
while ( ptrn.matches( text, matches, lastPos ) ) {
// Text before the file
if ( (size_t)matches[0].start > lastPos ) {
j.push_back(
{ { "type", "text" }, { "text", text.substr( lastPos, matches[0].start - lastPos ) } } );
}
std::string path( text.substr( matches[1].start, matches[1].length() ) );
if ( FileSystem::isRelativePath( path ) ) {
std::string prjPath( getPlugin()->getPluginContext()->getCurrentProject() );
path = prjPath + path;
}
std::string fileBuffer;
if ( FileSystem::fileGet( path, fileBuffer ) ) {
j.push_back( { { "type", "resource" },
{ "resource", { { "path", path }, { "content", fileBuffer } } } } );
}
lastPos = matches[0].end;
}
if ( lastPos < text.size() ) {
j.push_back( { { "type", "text" }, { "text", text.substr( lastPos ) } } );
}
if ( j.empty() && !text.empty() ) {
j.push_back( { { "type", "text" }, { "text", text } } );
}
return j;
}
nlohmann::json LLMChatUI::chatToJson( bool forRequest ) {
auto j = nlohmann::json::array();
auto chats = findAllByClass( "llm_conversation" );
@@ -1278,6 +1419,10 @@ nlohmann::json LLMChatUI::serialize() {
std::string inputText( mChatInput->getDocument().getText().toUtf8() );
j["input"] = std::move( inputText );
j["locked"] = mChatLocked;
j["agent_mode"] = mIsAgentMode;
j["agent_name"] = mCurAgent;
if ( mAgentSession && !mAgentSession->getSessionId().empty() )
j["session_id"] = mAgentSession->getSessionId();
return j;
}
@@ -1288,6 +1433,8 @@ std::string LLMChatUI::unserialize( const nlohmann::json& payload ) {
mTimestamp = payload.value( "timestamp", 0 );
mSummary = payload.value( "summary", "" );
mChatLocked = payload.value( "locked", false );
mIsAgentMode = payload.value( "agent_mode", false );
mCurAgent = payload.value( "agent_name", "" );
std::string provider = payload.value( "provider", "" );
if ( payload.contains( "chat" ) && payload["chat"].is_object() ) {
@@ -1296,11 +1443,30 @@ std::string LLMChatUI::unserialize( const nlohmann::json& payload ) {
mCurModel = findModel( provider, model );
}
if ( mCurModel.name.empty() )
if ( mCurModel.name.empty() && !mIsAgentMode )
return payload.value( "input", "" );
if ( !selectModel( mModelDDL, mCurModel ) )
fillModelDropDownList( mModelDDL );
if ( mIsAgentMode ) {
mChatAgentMode->setSelected( true );
updateAgentModeUI();
if ( !mCurAgent.empty() ) {
auto index = mAgentDDL->getListBox()->getItemIndex( mCurAgent );
if ( index != eeINDEX_NOT_FOUND )
mAgentDDL->getListBox()->setSelected( index );
}
std::string sessionId = payload.value( "session_id", "" );
if ( !sessionId.empty() ) {
auto it = mAgents.find( mCurAgent );
if ( it != mAgents.end() ) {
setupAgentSession();
mAgentSession->startLoaded( sessionId, []( bool ) {} );
}
}
} else {
if ( !selectModel( mModelDDL, mCurModel ) )
fillModelDropDownList( mModelDDL );
}
if ( payload.contains( "chat" ) && payload["chat"].is_object() ) {
const auto& chat = payload["chat"];
@@ -1696,6 +1862,13 @@ UIWidget* LLMChatUI::addChatUI( LLMChat::Role role ) {
i18n( "chat_copied", "Chat Copied" ) );
} );
resizeToFit( editor );
if ( mIsAgentMode ) {
chat->findByClass( "role_ui" )->setVisible( false );
chat->findByClass( "erase_but" )->setVisible( false );
chat->findByClass( "move_up" )->setVisible( false );
chat->findByClass( "move_down" )->setVisible( false );
}
return chat;
}
@@ -1770,6 +1943,9 @@ void LLMChatUI::updateTabTitle() {
return;
UITab* tab = reinterpret_cast<UITab*>( getData() );
auto title = i18n( "ai_assistant", "AI Assistant" );
if ( mIsAgentMode && !mCurAgent.empty() ) {
title = i18n( "agent", "Agent" ) + ": " + mCurAgent;
}
if ( !mSummary.empty() )
title += " - " + mSummary;
tab->setText( title );

View File

@@ -129,6 +129,12 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
std::map<std::string, ACPAgent> mAgents;
std::string mCurAgent;
struct SlashCommand {
std::string name;
std::string description;
};
std::vector<SlashCommand> mAvailableCommands;
std::unique_ptr<acp::AgentSession> mAgentSession;
int mPendingModelsToLoad{ 0 };
@@ -180,6 +186,10 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
void fillAgentDropDownList( UIDropDownList* agentDDL );
void updateAgentModeUI();
void setupAgentSession();
void resizeToFit( UICodeEditor* editor );
void addChat( LLMChat::Role role, std::string conversation );
@@ -221,6 +231,8 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter {
void replaceFileLinksToContents( std::string& text );
nlohmann::json promptToContentBlocks( std::string text );
void generateChatName( bool isRenaming );
void regenerateChatName();