diff --git a/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp b/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp index 3900064e8..c02af7512 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp +++ b/src/tools/ecode/plugins/aiassistant/acp/acpclient.cpp @@ -152,6 +152,8 @@ void ACPClient::processResponse( const json& msg ) { if ( handler ) { handler( id, msg ); + } else if ( msg.contains( "error" ) && onError ) { + onError( ResponseError( msg["error"] ) ); } } @@ -194,54 +196,74 @@ void ACPClient::sendError( const json& id, int code, const std::string& message } } -void ACPClient::initialize( const InitializeRequest& req, - const std::function& cb ) { +void ACPClient::initialize( + const InitializeRequest& req, + const std::function& )>& + cb ) { write( { { "method", "initialize" }, { "params", req.toJson() } }, [this, cb]( const IdType&, const json& resp ) { if ( resp.contains( "result" ) ) { mReady = true; if ( cb ) - cb( InitializeResponse( resp["result"] ) ); + cb( InitializeResponse( resp["result"] ), std::nullopt ); + } else if ( resp.contains( "error" ) ) { + if ( cb ) + cb( {}, ResponseError( resp["error"] ) ); } } ); } -void ACPClient::newSession( const NewSessionRequest& req, - const std::function& cb ) { +void ACPClient::newSession( + const NewSessionRequest& req, + const std::function& )>& + cb ) { write( { { "method", "session/new" }, { "params", req.toJson() } }, [cb]( const IdType&, const json& resp ) { if ( resp.contains( "result" ) && cb ) { - cb( NewSessionResponse( resp["result"] ) ); + cb( NewSessionResponse( resp["result"] ), std::nullopt ); + } else if ( resp.contains( "error" ) && cb ) { + cb( {}, ResponseError( resp["error"] ) ); } } ); } -void ACPClient::loadSession( const LoadSessionRequest& req, - const std::function& cb ) { +void ACPClient::loadSession( + const LoadSessionRequest& req, + const std::function& )>& + cb ) { write( { { "method", "session/load" }, { "params", req.toJson() } }, [cb]( const IdType&, const json& resp ) { if ( resp.contains( "result" ) && cb ) { - cb( LoadSessionResponse( resp["result"] ) ); + cb( LoadSessionResponse( resp["result"] ), std::nullopt ); + } else if ( resp.contains( "error" ) && cb ) { + cb( {}, ResponseError( resp["error"] ) ); } } ); } -void ACPClient::listSessions( const ListSessionsRequest& req, - const std::function& cb ) { +void ACPClient::listSessions( + const ListSessionsRequest& req, + const std::function& )>& + cb ) { write( { { "method", "session/list" }, { "params", req.toJson() } }, [cb]( const IdType&, const json& resp ) { if ( resp.contains( "result" ) && cb ) { - cb( ListSessionsResponse( resp["result"] ) ); + cb( ListSessionsResponse( resp["result"] ), std::nullopt ); + } else if ( resp.contains( "error" ) && cb ) { + cb( {}, ResponseError( resp["error"] ) ); } } ); } -void ACPClient::prompt( const PromptRequest& req, - const std::function& cb ) { +void ACPClient::prompt( + const PromptRequest& req, + const std::function& )>& cb ) { write( { { "method", "session/prompt" }, { "params", req.toJson() } }, [cb]( const IdType&, const json& resp ) { if ( resp.contains( "result" ) && cb ) { - cb( PromptResponse( resp["result"] ) ); + cb( PromptResponse( resp["result"] ), std::nullopt ); + } else if ( resp.contains( "error" ) && cb ) { + cb( {}, ResponseError( resp["error"] ) ); } } ); } diff --git a/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp b/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp index 98dab8993..57af63cf2 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp +++ b/src/tools/ecode/plugins/aiassistant/acp/acpclient.hpp @@ -44,19 +44,26 @@ class ACPClient { const Config& getConfig() const { return mConfig; } void initialize( const InitializeRequest& req, - const std::function& cb ); + const std::function& )>& cb ); void newSession( const NewSessionRequest& req, - const std::function& cb ); + const std::function& )>& cb ); void loadSession( const LoadSessionRequest& req, - const std::function& cb ); + const std::function& )>& cb ); void listSessions( const ListSessionsRequest& req, - const std::function& cb ); - void prompt( const PromptRequest& req, const std::function& cb ); + const std::function& )>& cb ); + void prompt( const PromptRequest& req, + const std::function& )>& cb ); // Notifications to agent void cancel( const std::string& sessionId ); // Callbacks from agent + std::function onError; std::function onSessionUpdate; std::function )> diff --git a/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp b/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp index 4b66363e0..e4ffb85da 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp +++ b/src/tools/ecode/plugins/aiassistant/acp/acpprotocol.hpp @@ -105,6 +105,22 @@ struct ListSessionsResponse { ListSessionsResponse( const json& body ); }; +struct ResponseError { + int code{ 0 }; + std::string message; + json data; + + ResponseError() = default; + ResponseError( const json& body ) { + if ( body.contains( "code" ) ) + code = body["code"].get(); + if ( body.contains( "message" ) ) + message = body["message"].get(); + if ( body.contains( "data" ) ) + data = body["data"]; + } +}; + struct PromptRequest { std::string sessionId; json prompt; // Array of ContentBlock diff --git a/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp b/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp index cd72a865e..629a35d62 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp +++ b/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp @@ -21,15 +21,28 @@ bool AgentSession::start( const std::function& onReady ) { req.clientCapabilities.fsReadTextFile = true; req.clientCapabilities.fsWriteTextFile = true; - mClient->initialize( req, [this, onReady]( const InitializeResponse& ) { - NewSessionRequest nreq; - nreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : ""; - mClient->newSession( nreq, [this, onReady]( const NewSessionResponse& nres ) { - mSessionId = nres.sessionId; - if ( onReady ) - onReady( true ); + mClient->initialize( + req, [this, onReady]( const InitializeResponse&, const std::optional& err ) { + if ( err ) { + if ( onReady ) + onReady( false ); + return; + } + NewSessionRequest nreq; + nreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : ""; + mClient->newSession( + nreq, [this, onReady]( const NewSessionResponse& nres, + const std::optional& err ) { + if ( err ) { + if ( onReady ) + onReady( false ); + return; + } + mSessionId = nres.sessionId; + if ( onReady ) + onReady( true ); + } ); } ); - } ); return true; } if ( onReady ) @@ -45,23 +58,35 @@ bool AgentSession::startLoaded( const std::string& sessionId, 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; + mClient->initialize( + req, [this, sessionId, onReady]( const InitializeResponse& ires, + const std::optional& err ) { + if ( err ) { 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 ); - } - } ); + onReady( false ); + return; + } + if ( ires.agentCapabilities.loadSession ) { + LoadSessionRequest lreq; + lreq.sessionId = sessionId; + lreq.cwd = mClient->isReady() ? mClient->getConfig().workingDirectory : ""; + mClient->loadSession( + lreq, [this, sessionId, onReady]( const LoadSessionResponse&, + const std::optional& err ) { + if ( err ) { + if ( onReady ) + onReady( false ); + return; + } + mSessionId = sessionId; + if ( onReady ) + onReady( true ); + } ); + } else { + if ( onReady ) + onReady( false ); + } + } ); return true; } if ( onReady ) @@ -69,16 +94,21 @@ bool AgentSession::startLoaded( const std::string& sessionId, return false; } -void AgentSession::listSessions( const std::function& )>& cb ) { +void AgentSession::listSessions( + const std::function&, const std::optional& )>& + cb ) { if ( !mClient->isReady() ) { - if ( cb ) cb( {} ); + if ( cb ) + cb( {}, std::nullopt ); return; } ListSessionsRequest req; req.cwd = mClient->getConfig().workingDirectory; - mClient->listSessions( req, [cb]( const ListSessionsResponse& res ) { - if ( cb ) cb( res.sessions ); - } ); + mClient->listSessions( + req, [cb]( const ListSessionsResponse& res, const std::optional& err ) { + if ( cb ) + cb( res.sessions, err ); + } ); } void AgentSession::stop() { @@ -86,14 +116,16 @@ void AgentSession::stop() { mClient->stop(); } -void AgentSession::prompt( const PromptRequest& req, - const std::function& cb ) { +void AgentSession::prompt( + const PromptRequest& req, + const std::function& )>& cb ) { mIsPrompting = true; - mClient->prompt( req, [this, cb](const PromptResponse& res) { - mIsPrompting = false; - if ( cb ) - cb(res); - } ); + mClient->prompt( + req, [this, cb]( const PromptResponse& res, const std::optional& err ) { + mIsPrompting = false; + if ( cb ) + cb( res, err ); + } ); } void AgentSession::cancel() { @@ -108,6 +140,11 @@ void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* u } void AgentSession::setupClient() { + mClient->onError = [this]( const ResponseError& err ) { + if ( onError ) + onError( err ); + }; + mClient->onSessionUpdate = [this]( const json& msg ) { 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 79aaee670..4093f1390 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp +++ b/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp @@ -22,14 +22,19 @@ class AgentSession { bool start( const std::function& onReady ); bool startLoaded( const std::string& sessionId, const std::function& onReady ); - void listSessions( const std::function& )>& cb ); + void listSessions( + const std::function&, const std::optional& )>& + cb ); void stop(); - void prompt( const PromptRequest& req, const std::function& cb ); + void prompt( const PromptRequest& req, + const std::function& )>& + cb ); void cancel(); bool isPrompting() const { return mIsPrompting; } + std::function onError; std::function onSessionUpdate; std::function )> diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 6f68404a9..3d71f6917 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -281,8 +281,11 @@ class AgentSessionHistoryModel : public Model { switch ( index.column() ) { case Columns::Title: { - return Variant( session.title.empty() ? "Untitled Session" - : session.title.c_str() ); + return Variant( session.title.empty() + ? ( mUISceneNode ? mUISceneNode->i18n( "untitled_session", + "Untitled Session" ) + : "Untitled Session" ) + : session.title.c_str() ); } case Columns::UpdatedAt: { return Variant( session.updatedAt.c_str() ); @@ -532,9 +535,10 @@ static const char* DEFAULT_CHAT_GLOBE = R"xml( static const char* DEFAULT_PERMISSION_GLOBE = R"xml( - - - + + + + @@ -561,6 +565,7 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : mChatAgentMode = find( "llm_agent_mode" ); mChatAgentMode->on( Event::OnValueChange, [this]( auto ) { mIsAgentMode = mChatAgentMode->isSelected(); + updateTabTitle(); updateAgentModeUI(); } ); @@ -1232,9 +1237,34 @@ void LLMChatUI::showChatHistory() { UISceneNode* uiSceneNode = getUISceneNode(); if ( mIsAgentMode ) { + auto listSessionsCb = [winId, uiSceneNode, tv, loader, input]( + const std::vector& sessions, + const std::optional& err ) { + uiSceneNode->runOnMainThread( [winId, uiSceneNode, tv, loader, input, sessions, err] { + auto win = uiSceneNode->find( winId ); + if ( win == nullptr ) + return; + if ( err ) { + loader->setVisible( false ); + NotificationCenter::instance()->addNotification( + uiSceneNode->i18n( "ai_assistant_agent_error", "Agent Error: " ) + + err->message ); + win->asType()->closeWindow(); + return; + } + auto model = std::make_shared( sessions, uiSceneNode ); + loader->setVisible( false ); + input->setVisible( true ); + tv->setVisible( true ); + tv->setModel( model ); + input->setFocus(); + } ); + }; + if ( !mAgentSession ) { setupAgentSession(); - mAgentSession->start( [this, winId, uiSceneNode, tv, loader, input]( bool ready ) { + + mAgentSession->start( [this, winId, uiSceneNode, loader, listSessionsCb]( bool ready ) { if ( !ready ) { uiSceneNode->runOnMainThread( [loader, winId, uiSceneNode] { if ( uiSceneNode->find( winId ) == nullptr ) @@ -1243,37 +1273,10 @@ void LLMChatUI::showChatHistory() { } ); return; } - mAgentSession->listSessions( [winId, uiSceneNode, tv, loader, input]( - const std::vector& sessions ) { - uiSceneNode->runOnMainThread( - [sessions, winId, uiSceneNode, tv, loader, input] { - if ( uiSceneNode->find( winId ) == nullptr ) - return; - auto model = - std::make_shared( sessions, uiSceneNode ); - loader->setVisible( false ); - input->setVisible( true ); - tv->setVisible( true ); - tv->setModel( model ); - input->setFocus(); - } ); - } ); + mAgentSession->listSessions( listSessionsCb ); } ); } else { - mAgentSession->listSessions( [winId, uiSceneNode, tv, loader, - input]( const std::vector& sessions ) { - uiSceneNode->runOnMainThread( [sessions, winId, uiSceneNode, tv, loader, input] { - if ( uiSceneNode->find( winId ) == nullptr ) - return; - auto model = - std::make_shared( sessions, uiSceneNode ); - loader->setVisible( false ); - input->setVisible( true ); - tv->setVisible( true ); - tv->setModel( model ); - input->setFocus(); - } ); - } ); + mAgentSession->listSessions( listSessionsCb ); } } else { getUISceneNode()->getThreadPool()->run( @@ -1596,10 +1599,11 @@ void LLMChatUI::setupAgentSession() { writeToLastChat( chunk ); } } else if ( sessionUpdate == "tool_call" ) { - std::string toolStr = "\n> Tool Call: " + msg.value( "title", "" ) + "\n"; + std::string toolStr = "\n> " + i18n( "tool_call", "Tool Call: " ) + + msg.value( "title", "" ) + "\n"; writeToLastChat( toolStr ); } else if ( sessionUpdate == "plan" ) { - std::string planStr = "\n> Plan Updated:\n"; + std::string planStr = "\n> " + i18n( "plan_updated", "Plan Updated:" ) + "\n"; if ( msg.contains( "plan" ) && msg["plan"].contains( "steps" ) && msg["plan"]["steps"].is_array() ) { for ( const auto& step : msg["plan"]["steps"] ) { @@ -1623,7 +1627,13 @@ void LLMChatUI::setupAgentSession() { runOnMainThread( [this, req, cb]() { addPermissionUI( req, cb ); } ); }; + mAgentSession->onError = [this]( const acp::ResponseError& err ) { + NotificationCenter::instance()->addNotification( + i18n( "ai_assistant_agent_error", "Agent Error: " ) + err.message ); + }; + mAgentSession->onTerminalCreated = [this]( const acp::CreateTerminalRequest& req, + const std::string& termId ) { runOnMainThread( [this, req, termId] { find( "chat_presentation" )->setVisible( false ); @@ -1755,7 +1765,22 @@ void LLMChatUI::sendAgentPrompt() { req.prompt = { { { "type", "text" }, { "text", "" } } }; } - mAgentSession->prompt( req, [this]( const acp::PromptResponse& res ) { + mAgentSession->prompt( req, [this]( const acp::PromptResponse& res, + const std::optional& err ) { + if ( err ) { + runOnMainThread( [this] { + mChatStop->setVisible( false )->setEnabled( false ); + mChatRun->setVisible( true )->setEnabled( true ); + toggleEnableChats( true ); + auto lastChat = getLastConversation(); + if ( lastChat ) { + auto* thinking = lastChat->findByClass( "thinking" ); + if ( thinking ) + thinking->setVisible( false ); + } + } ); + return; + } runOnMainThread( [this, res]() { auto chat = getLastConversation(); if ( chat ) { @@ -2241,14 +2266,15 @@ void LLMChatUI::addPermissionUI( const acp::RequestPermissionRequest& req, mChatsList->getUISceneNode()->loadLayoutFromString( DEFAULT_PERMISSION_GLOBE, mChatsList ); UITextView* desc = chat->findByClass( "permission_desc" ); - std::string descStr = "The agent wants to execute a tool call:\n"; - descStr += "Title: " + req.toolCall.title + "\n"; - descStr += "Kind: " + req.toolCall.kind + "\n"; + std::string descStr = + i18n( "agent_wants_to_execute_tool_call", "The agent wants to execute a tool call:" ) + + "\n"; + descStr += i18n( "title", "Title" ) + ": " + req.toolCall.title + "\n"; + descStr += i18n( "kind", "Kind" ) + ": " + req.toolCall.kind + "\n"; if ( !req.toolCall.rawInput.is_null() ) { - descStr += "Input:\n" + req.toolCall.rawInput.dump( 2 ) + "\n"; + descStr += i18n( "input", "Input" ) + ":\n" + req.toolCall.rawInput.dump( 2 ) + "\n"; } desc->setText( descStr ); - UIWidget* optionsBox = chat->findByClass( "permission_options" ); bool isFirst = true;