diff --git a/bin/assets/plugins/aiassistant.json b/bin/assets/plugins/aiassistant.json index 671039058..b190a2ef3 100644 --- a/bin/assets/plugins/aiassistant.json +++ b/bin/assets/plugins/aiassistant.json @@ -450,9 +450,8 @@ "display_name": "kimi-k2.5" }, { - "name": "stepfun/step-3.5-flash:free", - "display_name": "StepFun: Step 3.5 Flash (free)", - "cheapest": true + "name": "stepfun/step-3.5-flash", + "display_name": "StepFun: Step 3.5 Flash" }, { "name": "z-ai/glm-4.7", @@ -468,7 +467,8 @@ }, { "name": "arcee-ai/trinity-large-preview:free", - "display_name": "Arcee AI: Trinity Large Preview (free)" + "display_name": "Arcee AI: Trinity Large Preview (free)", + "cheapest": true }, { "name": "nvidia/nemotron-3-super-120b-a12b:free", @@ -496,6 +496,22 @@ { "name": "moonshotai/kimi-k2.5", "display_name": "kimi-k2.5" + }, + { + "name": "z-ai/glm4.7", + "display_name": "GLM-4.7" + }, + { + "name": "minimaxai/minimax-m2.7", + "display_name": "MiniMax M2.7" + }, + { + "name": "stepfun-ai/step-3.5-flash", + "display_name": "Step 3.5 Flash" + }, + { + "name": "deepseek-ai/deepseek-v3.2", + "display_name": "DeepSeek-V3.2" } ] }, diff --git a/include/eepp/graphics.hpp b/include/eepp/graphics.hpp index dcc20c8b5..ba8bdf8d0 100644 --- a/include/eepp/graphics.hpp +++ b/include/eepp/graphics.hpp @@ -58,6 +58,7 @@ #include #include #include +#include #include #include #include diff --git a/include/eepp/network/http.hpp b/include/eepp/network/http.hpp index f9c6d22ce..246b7b564 100644 --- a/include/eepp/network/http.hpp +++ b/include/eepp/network/http.hpp @@ -340,7 +340,7 @@ class EE_API Http : NonCopyable { const CancelCallback& getCancelCallback() const; /** Cancels the current request if being processed */ - void cancel(); + void cancel( bool resetCancelCallback = false ); /** @return True if the current request was cancelled */ const bool& isCancelled() const; @@ -546,7 +546,7 @@ class EE_API Http : NonCopyable { bool isProxied() const; /** @return If request has been found and canceled */ - bool setCancelRequest( Uint64 reqId ); + bool setCancelRequest( Uint64 reqId, bool resetCancelCallback = false ); /** Helper class to build the body of a multipart/form-data request. */ class EE_API MultipartEntitiesBuilder { @@ -721,7 +721,7 @@ class EE_API Http : NonCopyable { Uint64 id() const { return mId; } - void cancel(); + void cancel( bool resetCancelCallback = false ); protected: friend class Http; diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index 1d27b6b43..9ef72a46a 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -116,6 +116,7 @@ #include #include #include +#include #include #include #include diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index 926a17bc5..ed8bce07c 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -210,9 +210,11 @@ const Http::Request::CancelCallback& Http::Request::getCancelCallback() const { return mCancelCallback; } -void Http::Request::cancel() { +void Http::Request::cancel( bool resetCancelCallback ) { mCancel = true; setProgressCallback( {} ); + if ( resetCancelCallback ) + setCancelCallback( {} ); } const bool& Http::Request::isCancelled() const { @@ -1264,8 +1266,8 @@ Http::AsyncRequest::~AsyncRequest() { eeSAFE_DELETE( mStream ); } -void Http::AsyncRequest::cancel() { - mRequest.cancel(); +void Http::AsyncRequest::cancel( bool resetCancelCallback ) { + mRequest.cancel( resetCancelCallback ); } void Http::AsyncRequest::run() { @@ -1359,11 +1361,11 @@ bool Http::isProxied() const { return !mProxy.empty(); } -bool Http::setCancelRequest( Uint64 reqId ) { +bool Http::setCancelRequest( Uint64 reqId, bool resetCancelCallback ) { Lock l( mCurRequestsMutex ); auto found = mCurRequests.find( reqId ); if ( found != mCurRequests.end() ) { - found->second->cancel(); + found->second->cancel( resetCancelCallback ); return true; } return false; diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 383a03412..3aa0f749a 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -1145,67 +1145,13 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseUR } URI UISceneNode::solveRelativePath( URI uri, URI baseURI ) { - // Determine which base URI to use contextually - const URI& base = baseURI.empty() ? mURI : baseURI; + URI base = baseURI.empty() ? mURI : baseURI; - // 1. If base is empty OR the target already has a scheme (it's absolute), return it. - if ( base.empty() || !uri.getScheme().empty() ) - return uri; + // Automatically handles absolute URLs, protocol-relative URLs, + // directory merging, and ".." segment collapsing! + base.resolve( uri ); - std::string targetPath = uri.getPath(); - - if ( targetPath.length() >= 2 && targetPath[0] == '/' && targetPath[1] == '/' ) { - // Find where the authority ends and the real path begins - size_t pathStart = targetPath.find( '/', 2 ); - - if ( pathStart != std::string::npos ) { - uri.setAuthority( targetPath.substr( 2, pathStart - 2 ) ); - uri.setPath( targetPath.substr( pathStart ) ); - } else { - uri.setAuthority( targetPath.substr( 2 ) ); - uri.setPath( "/" ); - } - - std::string baseScheme = base.getScheme(); - if ( baseScheme == "http" || baseScheme == "https" ) { - uri.setScheme( baseScheme ); - } else { - uri.setScheme( "https" ); - } - - return uri; - } - - // 2. Inherit Scheme and Authority for standard relative paths - if ( uri.getScheme().empty() ) { - // Use 'base' instead of 'mURI' - uri.setScheme( base.getScheme().empty() ? "file" : base.getScheme() ); - } - - if ( uri.getAuthority().empty() ) { - uri.setAuthority( base.getAuthority() ); - } - - // 3. Safely Resolve the Path - std::string basePath = base.getPath(); - - if ( !targetPath.empty() && targetPath.front() == '/' ) { - // CASE A: Root-relative path - uri.setPath( targetPath ); - } else { - // CASE B: Directory-relative path - size_t lastSlashPos = basePath.find_last_of( '/' ); - - if ( lastSlashPos != std::string::npos ) { - basePath = basePath.substr( 0, lastSlashPos + 1 ); - } else { - basePath = base.getAuthority().empty() ? "" : "/"; // Use 'base' - } - - uri.setPath( basePath + targetPath ); - } - - return uri; + return base; } void UISceneNode::loadCSS( URI uri ) { diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp index e1dc6adf9..3ccfececd 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp @@ -337,6 +337,11 @@ void AIAssistantPlugin::loadAIAssistantConfig( const std::string& path, bool upd if ( j.contains( "config" ) ) { auto& config = j["config"]; + if ( config.contains( "display_reasoning" ) && config["display_reasoning"].is_boolean() ) + mDisplayReasoning = config.value( "display_reasoning", false ); + else if ( updateConfigFile ) + config["display_reasoning"] = mDisplayReasoning; + if ( config.contains( "openai_api_key" ) ) mApiKeys["openai"] = config.value( "openai_api_key", "" ); else if ( updateConfigFile ) diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp index cf157fd45..827bf34cc 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp @@ -50,11 +50,14 @@ class AIAssistantPlugin : public PluginBase { void setConfig( AIAssistantConfig&& config ) { mConfig = std::move( config ); } + bool displayReasoning() const { return mDisplayReasoning; } + protected: LLMProviders mProviders; ACPAgents mAgents; bool mUIInit{ false }; bool mBrokenUserConfigFile{ false }; + bool mDisplayReasoning{ false }; UIWidget* mStatusBar{ nullptr }; UIPushButton* mAIChatButton{ nullptr }; UnorderedMap mApiKeys; diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 397247c3b..9c2d9ce12 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -600,7 +600,10 @@ static const char* DEFAULT_PERMISSION_GLOBE = R"xml( )xml"; LLMChatUI::LLMChatUI( PluginManager* manager ) : - UILinearLayout(), WidgetCommandExecuter( getInput() ), mManager( manager ) { + UILinearLayout(), + WidgetCommandExecuter( getInput() ), + mManager( manager ), + mDisplayReasoning( getPlugin() && getPlugin()->displayReasoning() ) { setClass( "llm_chatui" ); setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); @@ -776,7 +779,9 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : if ( mChatInput->getDocument().isEmpty() ) return; - addChat( LLMChat::stringToRole( mChatUserRole ), mChatInput->getDocument().toUtf8String() ); + auto chat = mChatInput->getDocument().toString(); + String::trimInPlace( chat, '\n' ); + addChat( LLMChat::stringToRole( mChatUserRole ), chat ); mChatInput->getDocument().selectAll(); mChatInput->getDocument().textInput( String{} ); mChatInput->setFocus(); @@ -2557,7 +2562,7 @@ void LLMChatUI::doRequest() { mThinkingBubble = nullptr; mRequest->streamedResponseCb = [this, editor, thinking, thinkingID]( const std::string& chunk, bool fromReasoning ) { - if ( fromReasoning ) { + if ( fromReasoning && !mDisplayReasoning ) { runOnMainThread( [this, chunk] { updateThinkingBubble( chunk ); } ); return; } @@ -2566,15 +2571,20 @@ void LLMChatUI::doRequest() { return; auto conversation = chunk; - editor->runOnMainThread( - [this, conversation = std::move( conversation ), editor, thinking, thinkingID] { - mThinkingBubble = nullptr; - editor->getDocument().textInput( String::fromUtf8( conversation ) ); - editor->setCursorVisible( false ); - thinking->removeActionsByTag( thinkingID ); - thinking->setVisible( false ); - resizeToFit( editor ); - } ); + editor->runOnMainThread( [this, conversation = std::move( conversation ), editor, thinking, + thinkingID]() mutable { + mThinkingBubble = nullptr; + if ( editor->getDocument().isEmpty() && !conversation.empty() && + conversation.front() == '\n' ) { + String::trimInPlace( conversation, '\n' ); + } + + editor->getDocument().textInput( String::fromUtf8( conversation ) ); + editor->setCursorVisible( false ); + thinking->removeActionsByTag( thinkingID ); + thinking->setVisible( false ); + resizeToFit( editor ); + } ); }; mRequest->cancelCb = [this, thinking, thinkingID, editor]( const LLMChatCompletionRequest& ) { @@ -3009,7 +3019,7 @@ UIWidget* LLMChatUI::addChatUI( LLMChat::Role role ) { return chat; } -void LLMChatUI::addChat( LLMChat::Role role, std::string conversation ) { +void LLMChatUI::addChat( LLMChat::Role role, const std::string& conversation ) { UIWidget* chat = addChatUI( role ); auto* editor = chat->findByClass( "data_ui" ); editor->getDocument().textInput( String::fromUtf8( conversation ) ); @@ -3019,6 +3029,16 @@ void LLMChatUI::addChat( LLMChat::Role role, std::string conversation ) { resizeToFit( editor ); } +void LLMChatUI::addChat( LLMChat::Role role, const String& conversation ) { + UIWidget* chat = addChatUI( role ); + auto* editor = chat->findByClass( "data_ui" ); + editor->getDocument().textInput( conversation ); + editor->setCursorVisible( false ); + editor->setAllowSelectingTextFromGutter( false ); + bindCmds( editor, false ); + resizeToFit( editor ); +} + void LLMChatUI::removeLastChat() { auto* chat = getLastConversation(); if ( chat ) { diff --git a/src/tools/ecode/plugins/aiassistant/chatui.hpp b/src/tools/ecode/plugins/aiassistant/chatui.hpp index 0e7730177..688fc233c 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.hpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.hpp @@ -160,6 +160,7 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { bool mIsAgentMode{ false }; bool mChatLocked{ false }; bool mLinkMode{ false }; + bool mDisplayReasoning{ false }; std::vector mNewModels; LLMModel findModel( const std::string& provider, const std::string& model ); @@ -237,7 +238,9 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { void showSlashCommands(); - void addChat( LLMChat::Role role, std::string conversation ); + void addChat( LLMChat::Role role, const std::string& conversation ); + + void addChat( LLMChat::Role role, const String& conversation ); void writeToLastChat( const std::string& text ); diff --git a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp index ece7d6fba..f2df55412 100644 --- a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp +++ b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp @@ -90,17 +90,31 @@ LLMChatCompletionRequest::LLMChatCompletionRequest( const std::string& uri, cons const auto& reasoningContent = choice["delta"]["reasoning_content"]; if ( reasoningContent.is_string() ) { std::string delta = choice["delta"]["reasoning_content"]; - if ( streamedResponseCb ) - streamedResponseCb( delta, true ); - mReasoningResponse += std::move( delta ); + if ( !delta.empty() ) { + if ( streamedResponseCb ) + streamedResponseCb( delta, true ); + mReasoningResponse += std::move( delta ); + } + } + } else if ( choice["delta"].contains( "reasoning" ) ) { + const auto& reasoningContent = choice["delta"]["reasoning"]; + if ( reasoningContent.is_string() ) { + std::string delta = choice["delta"]["reasoning"]; + if ( !delta.empty() ) { + if ( streamedResponseCb ) + streamedResponseCb( delta, true ); + mReasoningResponse += std::move( delta ); + } } } else if ( choice["delta"].contains( "content" ) ) { const auto& content = choice["delta"]["content"]; if ( content.is_string() ) { std::string delta = choice["delta"]["content"]; - if ( streamedResponseCb ) - streamedResponseCb( delta, false ); - mResponse += std::move( delta ); + if ( !delta.empty() ) { + if ( streamedResponseCb ) + streamedResponseCb( delta, false ); + mResponse += std::move( delta ); + } } } } @@ -108,9 +122,11 @@ LLMChatCompletionRequest::LLMChatCompletionRequest( const std::string& uri, cons } else if ( data.contains( "delta" ) && data["delta"].value( "type", "" ) == "text_delta" ) { std::string delta = data["delta"]["text"]; - if ( streamedResponseCb ) - streamedResponseCb( delta, false ); - mResponse += std::move( delta ); + if ( !delta.empty() ) { + if ( streamedResponseCb ) + streamedResponseCb( delta, false ); + mResponse += std::move( delta ); + } } } ); @@ -123,7 +139,7 @@ LLMChatCompletionRequest::~LLMChatCompletionRequest() { cancelCb = nullptr; doneCb = nullptr; streamedResponseCb = nullptr; - cancel(); + cancel( true ); } void LLMChatCompletionRequest::request() { @@ -149,10 +165,10 @@ void LLMChatCompletionRequest::requestAsync() { mRequest, mStream, Seconds( 5 ) ); } -void LLMChatCompletionRequest::cancel() { +void LLMChatCompletionRequest::cancel(bool resetCancelCallback ) { mCancel = true; if ( mRequestId ) - mHttp->setCancelRequest( mRequestId ); + mHttp->setCancelRequest( mRequestId, resetCancelCallback ); if ( !mHadProgress ) onCancel(); } diff --git a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp index 1329eaec0..159e4be6d 100644 --- a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp +++ b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp @@ -36,7 +36,7 @@ class LLMChatCompletionRequest { void requestAsync(); - void cancel(); + void cancel( bool resetCancelCallback = false ); bool isCancelled() const;