diff --git a/bin/assets/plugins/aiassistant.json b/bin/assets/plugins/aiassistant.json index ac7dd7a55..df967cb10 100644 --- a/bin/assets/plugins/aiassistant.json +++ b/bin/assets/plugins/aiassistant.json @@ -520,6 +520,11 @@ "enabled": true, "command": "cursor", "args": ["acp"] + }, + "pi-acp": { + "enabled": true, + "command": "pi-acp", + "args": [] } } } diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index fccc3d095..5984b56bd 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -47,7 +47,7 @@ class LLMModelsModel : public Model { return mCurModels.size(); } - virtual size_t columnCount( const ModelIndex& = ModelIndex() ) const override { return 2; } + virtual size_t columnCount( const ModelIndex& = ModelIndex() ) const override { return 3; } virtual std::string columnName( const size_t& column ) const override { switch ( column ) { @@ -132,6 +132,106 @@ class LLMModelsModel : public Model { UISceneNode* mUISceneNode; }; +class ACPAgentsModel : public Model { + public: + enum Columns { Name, Command }; + + ACPAgentsModel( const std::map& agents, + UISceneNode* uiSceneNode = nullptr ) : + mAgents( agents ), mUISceneNode( uiSceneNode ) { + mCurAgents.reserve( mAgents.size() ); + for ( const auto& [name, agent] : mAgents ) { + if ( agent.enabled ) + mCurAgents.emplace_back( name ); + } + } + + virtual size_t rowCount( const ModelIndex& = ModelIndex() ) const override { + return mCurAgents.size(); + } + + virtual size_t columnCount( const ModelIndex& = ModelIndex() ) const override { return 2; } + + virtual std::string columnName( const size_t& column ) const override { + switch ( column ) { + case Columns::Name: + return mUISceneNode ? mUISceneNode->i18n( "name", "Name" ) : "Name"; + case Columns::Command: + return mUISceneNode ? mUISceneNode->i18n( "command", "Command" ) : "Command"; + } + return ""; + } + + virtual Variant data( const ModelIndex& index, + ModelRole role = ModelRole::Display ) const override { + if ( role != ModelRole::Display ) + return {}; + + if ( index.row() < 0 || static_cast( index.row() ) >= mCurAgents.size() ) + return {}; + + const auto& agentName = mCurAgents[index.row()]; + auto it = mAgents.find( agentName ); + if ( it == mAgents.end() ) + return {}; + + switch ( index.column() ) { + case Columns::Name: { + return Variant( it->second.name.c_str() ); + } + case Columns::Command: { + return Variant( it->second.command.c_str() ); + } + } + + return {}; + } + + ModelIndex getFromName( const std::string& name ) { + auto it = + std::find_if( mCurAgents.begin(), mCurAgents.end(), + [&name]( const std::string& agentName ) { return name == agentName; } ); + return it != mCurAgents.end() ? index( std::distance( mCurAgents.begin(), it ) ) + : index( 0 ); + } + + void setFilter( const std::string& filter ) { + if ( mCurFilter == filter ) + return; + mCurFilter = filter; + mCurAgents.clear(); + + for ( const auto& [name, agent] : mAgents ) { + if ( !agent.enabled ) + continue; + + if ( filter.empty() ) { + mCurAgents.emplace_back( name ); + continue; + } + + bool matchesName = String::icontains( agent.name, filter ); + bool matchesCommand = String::icontains( agent.command, filter ); + + if ( matchesName || matchesCommand ) { + mCurAgents.emplace_back( name ); + } + } + + invalidate(); + } + + const std::vector& getCurAgents() const { return mCurAgents; } + + void refresh() { setFilter( mCurFilter ); } + + protected: + const std::map& mAgents; + std::vector mCurAgents; + std::string mCurFilter; + UISceneNode* mUISceneNode; +}; + static const char* DEFAULT_PROVIDER = "google"; static const char* DEFAULT_MODEL = "gemini-2.5-flash"; @@ -234,16 +334,19 @@ DropDownList.role_ui { tint: var(--font); } .llm_chat_attach, -.llm_chat_select_model { +.llm_chat_select_model, +.llm_chat_select_agent { padding: 8dp 8dp 38dp 8dp; } .llm_chat_locate_input, -.llm_chat_select_model_input { +.llm_chat_select_model_input, +.llm_chat_select_agent_input { margin-bottom: 2dp; padding: 0 0 0 4dp; } .llm_chat_attach_locate, -.llm_chat_model_locate { +.llm_chat_model_locate, +.llm_chat_agent_locate { border-radius: 8dp; margin-bottom: 4dp; } @@ -296,16 +399,18 @@ DropDownList.role_ui { + + + + - - - - - + + + @@ -356,7 +461,9 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : mChatsList = findByClass( "llm_chats" ); mModelBtn = findByClass( "model_ui" ); - mAgentDDL = findByClass( "agent_ui" ); + mModelBtn->onClick( [this]( auto ) { execute( "ai-select-model" ); } ); + mAgentBtn = findByClass( "agent_ui" ); + mAgentBtn->onClick( [this]( auto ) { execute( "ai-select-agent" ); } ); mChatAgentMode = find( "llm_agent_mode" ); mChatAgentMode->on( Event::OnValueChange, [this]( auto ) { @@ -550,6 +657,26 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : hideSelectModel(); } ); + setCmd( "ai-select-agent", [this] { + if ( !mLocateAgentBarLayout->isVisible() ) { + if ( mLocateAgentTable->getModel() ) { + mLocateAgentInput->setText( "" ); + static_cast( mLocateAgentTable->getModel() )->setFilter( "" ); + } + + showSelectAgent(); + + mLocateAgentTable->runOnMainThread( [this] { + auto model = static_cast( mLocateAgentTable->getModel() ); + if ( model ) { + mLocateAgentTable->setSelection( model->getFromName( mCurAgent ) ); + mAgentBtn->setText( mCurAgent ); + } + } ); + } else + hideSelectAgent(); + } ); + setCmd( "ai-toggle-private-chat", [this] { mChatIsPrivate = !mChatIsPrivate; @@ -703,13 +830,12 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : mChatAttach = find( "llm_attach" ); mChatAttach->onClick( [this]( auto ) { execute( "ai-show-add-context-menu" ); } ); - mModelBtn->onClick( [this]( auto ) { execute( "ai-select-model" ); } ); - if ( getPlugin() == nullptr ) return; initAttachFile(); initSelectModel(); + initSelectAgent(); auto providers = getPlugin()->getProviders(); setProviders( std::move( providers ) ); @@ -735,13 +861,12 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : } if ( !agentName.empty() && mAgents.find( agentName ) != mAgents.end() ) { - mCurAgent = agentName; + selectAgent( agentName ); } else if ( !mAgents.empty() ) { - mCurAgent = mAgents.begin()->first; + selectAgent( mAgents.begin()->first ); } fillModelDropDownList(); - fillAgentDropDownList( mAgentDDL ); const auto appendShortcutToTooltip = [this]( UIPushButton* but, const std::string& cmd ) { auto kb = getKeyBindings().getCommandKeybindString( cmd ); @@ -853,6 +978,8 @@ void LLMChatUI::showChatHistory() { return; hideAttachFile(); + hideSelectModel(); + hideSelectAgent(); static const char* CHAT_HISTORY_LAYOUT = R"xml( model ) { return false; } +bool LLMChatUI::selectAgent( const std::string& agent ) { + if ( !agent.empty() && mAgents.find( agent ) != mAgents.end() ) { + mAgentBtn->setText( agent ); + mCurAgent = agent; + return true; + } + return false; +} void LLMChatUI::fillModelDropDownList() { mModels.clear(); std::size_t reserve = 0; @@ -1124,30 +1259,99 @@ void LLMChatUI::fillModelDropDownList() { getUISceneNode()->getThreadPool()->run( [this] { fillApiModels(); } ); } -void LLMChatUI::fillAgentDropDownList( UIDropDownList* agentDDL ) { - std::vector agents; - std::size_t selectedIndex = 0; - for ( const auto& [name, data] : mAgents ) { - if ( !data.enabled ) - continue; +// Agent Picker - String agentName( String::format( "%s", data.name ) ); - if ( data.name == mCurAgent ) - selectedIndex = agents.size(); - agents.push_back( std::move( agentName ) ); - } - agentDDL->getListBox()->clear(); - agentDDL->getListBox()->addListBoxItems( std::move( agents ) ); - agentDDL->getListBox()->setSelected( selectedIndex ); - agentDDL->on( Event::OnValueChange, [this, agentDDL]( auto ) { - auto newAgent = agentDDL->getListBox()->getItemSelectedText().toUtf8(); - if ( newAgent != mCurAgent ) { - mCurAgent = newAgent; - if ( mAgentSession ) { - mAgentSession->stop(); - mAgentSession.reset(); +void LLMChatUI::updateLocateAgentBarColumns() { + Float width = eeceil( mLocateAgentTable->getPixelsSize().getWidth() ); + width -= mLocateAgentTable->getVerticalScrollBar()->getPixelsSize().getWidth(); + mLocateAgentTable->setColumnsVisible( { 0 } ); + mLocateAgentTable->setColumnWidth( 0, eeceil( width ) ); +} + +void LLMChatUI::loadSelectAgent() { + auto ctx = getPlugin()->getPluginContext(); + mLocateAgentTable->setModel( + std::make_shared( mAgents, ctx->getUISceneNode() ) ); + + static_cast( mLocateAgentTable->getModel() ) + ->setFilter( mLocateAgentInput->getText().toUtf8() ); +} + +void LLMChatUI::showSelectAgent() { + if ( getPlugin() == nullptr ) + return; + hideAttachFile(); + hideSelectModel(); + + if ( nullptr == mLocateAgentTable->getModel() ) + loadSelectAgent(); + + static_cast( mLocateAgentTable->getModel() ) + ->setFilter( mLocateAgentInput->getText().toUtf8() ); + mLocateAgentBarLayout->setVisible( true ); + mLocateAgentInput->setFocus(); + updateLocateAgentBarColumns(); +} + +void LLMChatUI::hideSelectAgent() { + mLocateAgentBarLayout->setVisible( false ); +} + +void LLMChatUI::initSelectAgent() { + mLocateAgentBarLayout = findByClass( "llm_chat_select_agent" ); + mLocateAgentInput = findByClass( "llm_chat_select_agent_input" ); + mLocateAgentTable = findByClass( "llm_chat_agent_locate" ); + mLocateAgentTable->setHeadersVisible( false ); + + mLocateAgentTable->on( Event::OnSizeChange, + [this]( const Event* ) { updateLocateAgentBarColumns(); } ); + + mLocateAgentInput->on( Event::OnTextChanged, [this]( const Event* ) { + showSelectAgent(); + updateLocateAgentBarColumns(); + } ); + mLocateAgentInput->on( Event::OnPressEnter, [this]( const Event* ) { + KeyEvent keyEvent( mLocateAgentTable, Event::KeyDown, KEY_RETURN, SCANCODE_UNKNOWN, 0, 0 ); + mLocateAgentTable->forceKeyDown( keyEvent ); + } ); + mLocateAgentInput->on( Event::KeyDown, [this]( const Event* event ) { + const KeyEvent* keyEvent = static_cast( event ); + mLocateAgentTable->forceKeyDown( *keyEvent ); + } ); + mLocateAgentBarLayout->setCommand( "close-locatebar", [this] { + hideSelectAgent(); + if ( mChatInput ) + mChatInput->setFocus(); + } ); + mLocateAgentBarLayout->getKeyBindings().addKeybindsString( { + { "escape", "close-locatebar" }, + } ); + mLocateAgentTable->on( Event::KeyDown, [this]( const Event* event ) { + const KeyEvent* keyEvent = static_cast( event ); + if ( keyEvent->getKeyCode() == KEY_ESCAPE ) + mLocateAgentBarLayout->execute( "close-locatebar" ); + } ); + mLocateAgentTable->on( Event::OnModelEvent, [this]( const Event* event ) { + const ModelEvent* modelEvent = static_cast( event ); + if ( modelEvent->getModelEventType() == ModelEventType::Open ) { + Variant vName( modelEvent->getModel()->data( + modelEvent->getModel()->index( modelEvent->getModelIndex().row(), + ACPAgentsModel::Name ), + ModelRole::Display ) ); + + if ( vName.isValid() ) { + std::string name( vName.toString() ); + if ( name != mCurAgent ) { + selectAgent( name ); + if ( mAgentSession ) { + mAgentSession->stop(); + mAgentSession.reset(); + } + updateTabTitle(); + } } - updateTabTitle(); + + mLocateAgentBarLayout->execute( "close-locatebar" ); } } ); } @@ -1172,7 +1376,7 @@ void LLMChatUI::writeToLastChat( const std::string& text ) { void LLMChatUI::updateAgentModeUI() { mModelBtn->setVisible( !mIsAgentMode ); - mAgentDDL->setVisible( mIsAgentMode ); + mAgentBtn->setVisible( mIsAgentMode ); mChatAdd->setVisible( !mIsAgentMode ); mChatUserRole->setVisible( !mIsAgentMode ); @@ -1252,13 +1456,10 @@ void LLMChatUI::setupAgentSession() { auto* uiTerm = eterm::UI::UITerminal::New( getPlugin()->getPluginContext()->getTerminalFont(), - getPlugin() - ->getPluginContext() - ->termConfig() - .fontSize.asPixels( - getUISceneNode()->getPixelsSize().getWidth(), - getUISceneNode()->getPixelsSize(), getUISceneNode()->getDPI(), - getUISceneNode()->getUIThemeManager()->getDefaultFontSize() ), + 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 ); @@ -1351,19 +1552,17 @@ void LLMChatUI::sendAgentPrompt() { 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 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; - } ); + 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 } } }; + req.prompt = { + { { "type", "slash_command" }, { "name", cmdName }, { "argument", arg } } }; isSlashCommand = true; } } @@ -1454,8 +1653,8 @@ nlohmann::json LLMChatUI::promptToContentBlocks( std::string text ) { 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 ) } } ); + j.push_back( { { "type", "text" }, + { "text", text.substr( lastPos, matches[0].start - lastPos ) } } ); } std::string path( text.substr( matches[1].start, matches[1].length() ) ); @@ -1550,6 +1749,8 @@ std::string LLMChatUI::unserialize( const nlohmann::json& payload ) { mIsAgentMode = payload.value( "agent_mode", false ); mCurAgent = payload.value( "agent_name", "" ); + selectAgent( mCurAgent ); + std::string provider = payload.value( "provider", "" ); if ( payload.contains( "chat" ) && payload["chat"].is_object() ) { const auto& chat = payload["chat"]; @@ -1563,11 +1764,6 @@ std::string LLMChatUI::unserialize( const nlohmann::json& payload ) { 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() ) { @@ -2116,6 +2312,7 @@ void LLMChatUI::showAttachFile() { if ( getPlugin() == nullptr ) return; hideSelectModel(); + hideSelectAgent(); auto text = mLocateInput->getText(); auto ctx = getPlugin()->getPluginContext(); if ( !ctx->isDirTreeReady() ) { @@ -2274,6 +2471,7 @@ void LLMChatUI::showSelectModel() { if ( getPlugin() == nullptr ) return; hideAttachFile(); + hideSelectAgent(); if ( nullptr == mLocateModelTable->getModel() ) loadSelectModel(); diff --git a/src/tools/ecode/plugins/aiassistant/chatui.hpp b/src/tools/ecode/plugins/aiassistant/chatui.hpp index a07a19ef0..97a0039e0 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.hpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.hpp @@ -114,8 +114,8 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { UISelectButton* mChatPrivate{ nullptr }; UISelectButton* mChatAgentMode{ nullptr }; UIScrollView* mChatScrollView{ nullptr }; - UIDropDownList* mAgentDDL{ nullptr }; UIPushButton* mModelBtn{ nullptr }; + UIPushButton* mAgentBtn{ nullptr }; // Locate file UIVLinearLayoutCommandExecuter* mLocateBarLayout{ nullptr }; @@ -127,6 +127,11 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { UITextInput* mLocateModelInput{ nullptr }; UITableView* mLocateModelTable{ nullptr }; + // Select agent + UIVLinearLayoutCommandExecuter* mLocateAgentBarLayout{ nullptr }; + UITextInput* mLocateAgentInput{ nullptr }; + UITableView* mLocateAgentTable{ nullptr }; + std::unique_ptr mRequest; std::unique_ptr mSummaryRequest; LLMProviders mProviders; @@ -189,9 +194,19 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { bool selectModel( std::optional model ); + bool selectAgent( const std::string& agent ); + void fillModelDropDownList(); - void fillAgentDropDownList( UIDropDownList* agentDDL ); + void loadSelectAgent(); + + void showSelectAgent(); + + void initSelectAgent(); + + void hideSelectAgent(); + + void updateLocateAgentBarColumns(); void updateAgentModeUI();