diff --git a/bin/assets/fonts/codicon.ttf b/bin/assets/fonts/codicon.ttf index 0989abf8e..fe0b1d15e 100644 Binary files a/bin/assets/fonts/codicon.ttf and b/bin/assets/fonts/codicon.ttf differ diff --git a/src/eepp/ui/iconmanager.cpp b/src/eepp/ui/iconmanager.cpp index 6b3ef07cf..cd62f8318 100644 --- a/src/eepp/ui/iconmanager.cpp +++ b/src/eepp/ui/iconmanager.cpp @@ -299,6 +299,7 @@ UIIconTheme* IconManager::init( const std::string& iconThemeName, FontTrueType* { "chat-sparkle", 0xec4f }, { "inspect", 0xebd1 }, { "link", 0xeb15 }, + { "agent", 0xec67 }, } ) { iconTheme->add( UIGlyphIcon::New( icon.first, codIconFont, icon.second ) ); diff --git a/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp b/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp index c02af7512..eda871da1 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp +++ b/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp @@ -241,6 +241,63 @@ void ACPClient::loadSession( } ); } +void ACPClient::setConfigOption( + const SetConfigOptionRequest& req, + const std::function& )>& cb ) { + auto fallback = [this, req, cb]() { + if ( req.configId == "model" ) { + write( { { "method", "session/set_model" }, + { "params", { { "sessionId", req.sessionId }, { "modelId", req.optionId } } } }, + [req, cb]( const IdType&, const json& resp2 ) { + if ( resp2.contains( "result" ) && cb ) { + cb( SetConfigOptionResponse( resp2["result"], req.configId, + req.optionId ), + std::nullopt ); + } else if ( resp2.contains( "error" ) && cb ) { + cb( {}, ResponseError( resp2["error"] ) ); + } + } ); + } else if ( req.configId == "mode" ) { + write( { { "method", "session/set_mode" }, + { "params", { { "sessionId", req.sessionId }, { "modeId", req.optionId } } } }, + [req, cb]( const IdType&, const json& resp2 ) { + if ( resp2.contains( "result" ) && cb ) { + cb( SetConfigOptionResponse( resp2["result"], req.configId, + req.optionId ), + std::nullopt ); + } else if ( resp2.contains( "error" ) && cb ) { + cb( {}, ResponseError( resp2["error"] ) ); + } + } ); + } else if ( cb ) { + cb( {}, ResponseError{ -32601, "Method not found" } ); + } + }; + + if ( mLegacyConfigOnly ) { + fallback(); + return; + } + + write( { { "method", "session/set_config_option" }, { "params", req.toJson() } }, + [this, req, fallback, cb]( const IdType&, const json& resp ) { + if ( resp.contains( "result" ) && cb ) { + cb( SetConfigOptionResponse( resp["result"], req.configId, req.optionId ), + std::nullopt ); + } else if ( resp.contains( "error" ) ) { + ResponseError err( resp["error"] ); + if ( err.code == -32601 ) { + mLegacyConfigOnly = true; + fallback(); + return; + } + if ( cb ) + cb( {}, err ); + } + } ); +} + void ACPClient::listSessions( const ListSessionsRequest& req, const std::function& )>& diff --git a/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp b/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp index 57af63cf2..88e777689 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp +++ b/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp @@ -52,6 +52,9 @@ class ACPClient { void loadSession( const LoadSessionRequest& req, const std::function& )>& cb ); + void setConfigOption( const SetConfigOptionRequest& req, + const std::function& )>& cb ); void listSessions( const ListSessionsRequest& req, const std::function& )>& cb ); @@ -102,6 +105,7 @@ class ACPClient { std::map mHandlers; std::string mReceiveBuffer; + bool mLegacyConfigOnly{ false }; void readStdOut( const char* bytes, size_t n ); void readStdErr( const char* bytes, size_t n ); diff --git a/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.cpp b/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.cpp index cda4c0417..5bc6d7641 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.cpp +++ b/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.cpp @@ -2,6 +2,84 @@ namespace ecode { namespace acp { +json parseLegacyConfigOptions( const json& body, json configOptions, const std::string& forceId, + const std::string& forceValue ) { + if ( configOptions.is_null() ) { + configOptions = json::array(); + } + + auto updateOrAdd = [&configOptions]( json newOpt ) { + bool found = false; + for ( auto& opt : configOptions ) { + if ( opt.is_object() && opt.contains( "id" ) && opt["id"] == newOpt["id"] ) { + opt = std::move( newOpt ); + found = true; + break; + } + } + if ( !found ) { + configOptions.push_back( std::move( newOpt ) ); + } + }; + + if ( body.contains( "models" ) && body["models"].is_object() ) { + auto models = body["models"]; + if ( models.contains( "availableModels" ) && models["availableModels"].is_array() ) { + json modelConfig = { { "id", "model" }, + { "name", "Model" }, + { "type", "select" }, + { "category", "model" }, + { "options", json::array() } }; + for ( const auto& model : models["availableModels"] ) { + modelConfig["options"].push_back( + { { "id", model.value( "modelId", "" ) }, { "name", model.value( "name", "" ) } } ); + } + if ( models.contains( "currentModelId" ) ) { + modelConfig["currentValue"] = models.value( "currentModelId", "" ); + modelConfig["default"] = models.value( "currentModelId", "" ); + } else if ( !modelConfig["options"].empty() ) { + modelConfig["currentValue"] = modelConfig["options"][0]["id"]; + modelConfig["default"] = modelConfig["options"][0]["id"]; + } + updateOrAdd( std::move( modelConfig ) ); + } + } + + if ( body.contains( "modes" ) && body["modes"].is_object() ) { + auto modes = body["modes"]; + if ( modes.contains( "availableModes" ) && modes["availableModes"].is_array() ) { + json modeConfig = { { "id", "mode" }, + { "name", "Mode" }, + { "type", "select" }, + { "category", "mode" }, + { "options", json::array() } }; + for ( const auto& mode : modes["availableModes"] ) { + modeConfig["options"].push_back( + { { "id", mode.value( "id", "" ) }, { "name", mode.value( "name", "" ) } } ); + } + if ( modes.contains( "currentModeId" ) ) { + modeConfig["currentValue"] = modes.value( "currentModeId", "" ); + modeConfig["default"] = modes.value( "currentModeId", "" ); + } else if ( !modeConfig["options"].empty() ) { + modeConfig["currentValue"] = modeConfig["options"][0]["id"]; + modeConfig["default"] = modeConfig["options"][0]["id"]; + } + updateOrAdd( std::move( modeConfig ) ); + } + } + + if ( !forceId.empty() ) { + for ( auto& opt : configOptions ) { + if ( opt.is_object() && opt.contains( "id" ) && opt["id"] == forceId ) { + opt["currentValue"] = forceValue; + break; + } + } + } + + return configOptions; +} + ClientCapabilities::ClientCapabilities( const json& body ) { if ( body.contains( "terminal" ) ) terminal = body.value( "terminal", false ); @@ -58,6 +136,7 @@ NewSessionResponse::NewSessionResponse( const json& body ) { sessionId = body.value( "sessionId", "" ); if ( body.contains( "configOptions" ) ) configOptions = body["configOptions"]; + configOptions = parseLegacyConfigOptions( body, configOptions ); } json LoadSessionRequest::toJson() const { @@ -73,6 +152,18 @@ json LoadSessionRequest::toJson() const { LoadSessionResponse::LoadSessionResponse( const json& body ) { if ( body.contains( "configOptions" ) ) configOptions = body["configOptions"]; + configOptions = parseLegacyConfigOptions( body, configOptions ); +} + +json SetConfigOptionRequest::toJson() const { + return { { "sessionId", sessionId }, { "configId", configId }, { "optionId", optionId } }; +} + +SetConfigOptionResponse::SetConfigOptionResponse( const json& body, const std::string& configId, + const std::string& optionId ) { + if ( body.contains( "configOptions" ) ) + configOptions = body["configOptions"]; + configOptions = parseLegacyConfigOptions( body, configOptions, configId, optionId ); } SessionInfo::SessionInfo( const json& body ) { diff --git a/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp b/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp index e4ffb85da..f1dc19436 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp +++ b/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp @@ -11,6 +11,9 @@ using json = nlohmann::json; namespace ecode { namespace acp { +json parseLegacyConfigOptions( const json& body, json configOptions, const std::string& forceId = "", + const std::string& forceValue = "" ); + struct ClientCapabilities { bool terminal{ false }; bool fsReadTextFile{ false }; @@ -79,6 +82,23 @@ struct LoadSessionResponse { LoadSessionResponse( const json& body ); }; +struct SetConfigOptionRequest { + std::string sessionId; + std::string configId; + std::string optionId; + + SetConfigOptionRequest() = default; + json toJson() const; +}; + +struct SetConfigOptionResponse { + json configOptions; + + SetConfigOptionResponse() = default; + SetConfigOptionResponse( const json& body, const std::string& configId = "", + const std::string& optionId = "" ); +}; + struct SessionInfo { std::string sessionId; std::string cwd; @@ -111,6 +131,8 @@ struct ResponseError { json data; ResponseError() = default; + ResponseError( int code, std::string message, json data = {} ) : + code( code ), message( std::move( message ) ), data( std::move( data ) ) {} ResponseError( const json& body ) { if ( body.contains( "code" ) ) code = body["code"].get(); diff --git a/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp b/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp index 629a35d62..f8e30079e 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp +++ b/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp @@ -39,6 +39,7 @@ bool AgentSession::start( const std::function& onReady ) { return; } mSessionId = nres.sessionId; + mConfigOptions = nres.configOptions; if ( onReady ) onReady( true ); } ); @@ -71,7 +72,7 @@ bool AgentSession::startLoaded( const std::string& sessionId, lreq.sessionId = sessionId; lreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : ""; mClient->loadSession( - lreq, [this, sessionId, onReady]( const LoadSessionResponse&, + lreq, [this, sessionId, onReady]( const LoadSessionResponse& lres, const std::optional& err ) { if ( err ) { if ( onReady ) @@ -79,6 +80,7 @@ bool AgentSession::startLoaded( const std::string& sessionId, return; } mSessionId = sessionId; + mConfigOptions = lres.configOptions; if ( onReady ) onReady( true ); } ); @@ -146,6 +148,12 @@ void AgentSession::setupClient() { }; mClient->onSessionUpdate = [this]( const json& msg ) { + auto sessionUpdate = msg.value( "sessionUpdate", "" ); + if ( sessionUpdate == "config_options_update" && msg.contains( "configOptions" ) ) { + mConfigOptions = msg["configOptions"]; + } else if ( msg.contains( "models" ) || msg.contains( "modes" ) ) { + mConfigOptions = parseLegacyConfigOptions( msg, mConfigOptions ); + } if ( onSessionUpdate ) onSessionUpdate( msg ); }; diff --git a/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp b/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp index 4093f1390..bc5779e88 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp +++ b/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp @@ -44,6 +44,8 @@ class AgentSession { std::string getSessionId() const { return mSessionId; } ACPClient* getClient() const { return mClient.get(); } + json getConfigOptions() const { return mConfigOptions; } + void setConfigOptions( const json& opts ) { mConfigOptions = opts; } void setTerminalData( const std::string& terminalId, UITerminal* uiTerm ); @@ -51,6 +53,7 @@ class AgentSession { std::shared_ptr mThreadPool; std::unique_ptr mClient; std::string mSessionId; + json mConfigOptions; bool mIsPrompting{ false }; struct TermData { diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 3e515dc77..15abca831 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -532,6 +532,7 @@ DropDownList.role_ui { + @@ -598,6 +599,8 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : mModelBtn->onClick( [this]( auto ) { execute( "ai-select-model" ); } ); mAgentBtn = findByClass( "agent_ui" ); mAgentBtn->onClick( [this]( auto ) { execute( "ai-select-agent" ); } ); + mAgentConfigBtn = findByClass( "agent_config_ui" ); + mAgentConfigBtn->onClick( [this]( auto ) { showAgentConfigMenu(); } ); mChatAgentMode = find( "llm_agent_mode" ); mChatAgentMode->on( Event::OnValueChange, [this]( auto ) { @@ -1519,6 +1522,106 @@ void LLMChatUI::hideSelectAgent() { mLocateAgentBarLayout->setVisible( false ); } +void LLMChatUI::showAgentConfigMenu() { + if ( !mAgentSession || !mAgentSession->getClient()->isReady() ) { + if ( !mAgentSession ) + setupAgentSession(); + + if ( mAgentSession && !mAgentSession->getClient()->isReady() ) { + mAgentConfigBtn->setEnabled( false ); + mAgentSession->start( [this]( bool ready ) { + runOnMainThread( [this, ready]() { + mAgentConfigBtn->setEnabled( true ); + if ( ready ) { + showAgentConfigMenu(); + } else { + NotificationCenter::instance()->addNotification( + i18n( "failed_to_start_agent", "Failed to start agent process." ) ); + mAgentSession.reset(); + } + } ); + } ); + return; + } + return; + } + + auto configOptions = mAgentSession->getConfigOptions(); + if ( !configOptions.is_array() || configOptions.empty() ) { + NotificationCenter::instance()->addNotification( + i18n( "no_agent_configs", "No Config Options Available" ) ); + return; + } + + UIPopUpMenu* menu = UIPopUpMenu::New(); + + for ( const auto& opt : configOptions ) { + if ( !opt.is_object() || !opt.contains( "id" ) || !opt.contains( "name" ) || + !opt.contains( "options" ) || !opt["options"].is_array() ) + continue; + + UIPopUpMenu* subMenu = UIPopUpMenu::New(); + std::string optId = opt["id"].get(); + std::string currentVal = + opt.contains( "currentValue" ) ? opt["currentValue"].get() : ""; + + for ( const auto& subopt : opt["options"] ) { + if ( !subopt.is_object() || !subopt.contains( "id" ) || !subopt.contains( "name" ) ) + continue; + + std::string subId = subopt["id"].get(); + std::string subName = subopt["name"].get(); + + Drawable* icon = nullptr; + if ( subId == currentVal ) { + icon = getUISceneNode()->findIconDrawable( "ok", PixelDensity::dpToPxI( 12 ) ); + } + + auto* item = subMenu->add( subName, icon ); + item->setId( subId ); + } + + subMenu->on( Event::OnItemClicked, [this, optId]( const Event* event ) { + UIMenuItem* item = event->getNode()->asType(); + std::string subId( item->getId() ); + acp::SetConfigOptionRequest req; + req.sessionId = mAgentSession->getSessionId(); + req.configId = optId; + req.optionId = subId; + mAgentSession->getClient()->setConfigOption( + req, [this, optId, subId]( const acp::SetConfigOptionResponse& res, + const std::optional& err ) { + if ( err ) { + runOnMainThread( [this, err]() { + NotificationCenter::instance()->addNotification( + i18n( "agent_config_error", "Failed to update agent config: " ) + + err->message ); + } ); + } else { + auto newOpts = res.configOptions; + if ( newOpts.empty() ) { + newOpts = acp::parseLegacyConfigOptions( + {}, mAgentSession->getConfigOptions(), optId, subId ); + } + mAgentSession->setConfigOptions( newOpts ); + } + } ); + } ); + + menu->addSubMenu( opt["name"].get(), nullptr, subMenu ); + } + + if ( menu->getCount() == 0 ) { + menu->add( i18n( "no_agent_configs", "No Config Options Available" ) )->setEnabled( false ); + } + + menu->runOnMainThread( [this, menu] { + auto pos( mAgentConfigBtn->getScreenPos() ); + UIMenu::findBestMenuPos( pos, menu, nullptr, nullptr, mAgentConfigBtn ); + menu->showAtScreenPosition( pos ); + } ); +} + void LLMChatUI::initSelectAgent() { mLocateAgentBarLayout = findByClass( "llm_chat_select_agent" ); mLocateAgentInput = findByClass( "llm_chat_select_agent_input" ); @@ -1646,6 +1749,7 @@ void LLMChatUI::writeToLastChat( const std::string& text ) { void LLMChatUI::updateAgentModeUI() { mModelBtn->setVisible( !mIsAgentMode ); mAgentBtn->setVisible( mIsAgentMode ); + mAgentConfigBtn->setVisible( mIsAgentMode ); mChatAdd->setVisible( !mIsAgentMode ); mChatUserRole->setVisible( !mIsAgentMode ); @@ -1998,10 +2102,10 @@ nlohmann::json LLMChatUI::promptToContentBlocks( std::string text ) { path = prjPath + path; } - std::string fileBuffer; - if ( FileSystem::fileGet( path, fileBuffer ) ) { - j.push_back( { { "type", "resource" }, - { "resource", { { "path", path }, { "content", fileBuffer } } } } ); + if ( FileSystem::fileExists( path ) ) { + j.push_back( { { "type", "resource_link" }, + { "name", FileSystem::fileNameFromPath( path ) }, + { "uri", "file://" + path } } ); } lastPos = matches[0].end; diff --git a/src/tools/ecode/plugins/aiassistant/chatui.hpp b/src/tools/ecode/plugins/aiassistant/chatui.hpp index 9d1a8bc28..0fbc44a2e 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.hpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.hpp @@ -116,6 +116,7 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { UIScrollView* mChatScrollView{ nullptr }; UIPushButton* mModelBtn{ nullptr }; UIPushButton* mAgentBtn{ nullptr }; + UIPushButton* mAgentConfigBtn{ nullptr }; // Locate file UIVLinearLayoutCommandExecuter* mLocateBarLayout{ nullptr }; @@ -274,6 +275,8 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { void hideSelectModel(); + void showAgentConfigMenu(); + void insertFileToDocument( std::string path, std::shared_ptr cdoc ); void replaceFileLinksToContents( std::string& text );