This should fix two crashes:

* A crash during debugging that happens due to invalid memory access (UIAbstractView + VariablesModel changes).
* A crash on ChatUI when closing a chat that was about to receive a server response.
This commit is contained in:
Martín Lucas Golini
2026-03-06 00:53:05 -03:00
parent 6a9c2bbeeb
commit f2a70f9589
7 changed files with 76 additions and 37 deletions

View File

@@ -1,6 +1,7 @@
#ifndef EE_UI_UIABSTRACTVIEW_HPP
#define EE_UI_UIABSTRACTVIEW_HPP
#include <atomic>
#include <eepp/ui/keyboardshortcut.hpp>
#include <eepp/ui/models/model.hpp>
#include <eepp/ui/models/modeleditingdelegate.hpp>
@@ -150,6 +151,7 @@ class EE_API UIAbstractView : public UIScrollableWidget {
std::vector<KeyBindings::Shortcut> mEditShortcuts{ { KEY_F2 } };
SelectionType mSelectionType{ SelectionType::Row };
SelectionKind mSelectionKind{ SelectionKind::Single };
std::atomic<unsigned> mPendingUpdateFlags{ 0 };
virtual void editingWidgetDidChange( const ModelIndex& ) {}
};

View File

@@ -118,11 +118,12 @@ size_t UIAbstractTableView::getItemCount() const {
}
void UIAbstractTableView::onModelUpdate( unsigned flags ) {
mPendingUpdateFlags.fetch_or( flags );
if ( !Engine::instance()->isMainThread() ) {
removeActionsByTag( onModelUpdateTag );
runOnMainThread(
[this, flags] {
modelUpdate( flags );
[this] {
modelUpdate( mPendingUpdateFlags.exchange( 0 ) );
createOrUpdateColumns( true );
},
Time::Zero, onModelUpdateTag );

View File

@@ -4,6 +4,8 @@
namespace EE { namespace UI { namespace Abstract {
static constexpr String::HashType OnModelUpdateTag = String::hash( "onModelUpdate" );
UIAbstractView::UIAbstractView( const std::string& tag ) :
UIScrollableWidget( tag ), mSelection( this ) {}
@@ -126,7 +128,8 @@ void UIAbstractView::setModel( const std::shared_ptr<Model>& model ) {
}
void UIAbstractView::modelUpdate( unsigned flags ) {
if ( !getModel() || ( flags & Model::InvalidateAllIndexes ) ) {
mPendingUpdateFlags = 0;
if ( !getModel() || ( flags & Model::UpdateFlag::InvalidateAllIndexes ) ) {
getSelection().clear();
} else {
getSelection().removeAllMatching(
@@ -135,12 +138,13 @@ void UIAbstractView::modelUpdate( unsigned flags ) {
}
void UIAbstractView::onModelUpdate( unsigned flags ) {
mPendingUpdateFlags.fetch_or( flags );
if ( !Engine::instance()->isMainThread() ) {
static constexpr String::HashType tag = String::hash( "onModelUpdate" );
removeActionsByTag( tag );
runOnMainThread( [this, flags] { modelUpdate( flags ); }, Time::Zero, tag );
removeActionsByTag( OnModelUpdateTag );
runOnMainThread( [this] { modelUpdate( mPendingUpdateFlags.exchange( 0 ) ); }, Time::Zero,
OnModelUpdateTag );
} else {
modelUpdate( flags );
modelUpdate( mPendingUpdateFlags.exchange( 0 ) );
}
}

View File

@@ -595,6 +595,16 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
}
LLMChatUI::~LLMChatUI() {
if ( mRequest ) {
mRequest->cancelCb = nullptr;
mRequest->doneCb = nullptr;
mRequest->streamedResponseCb = nullptr;
}
if ( mSummaryRequest ) {
mSummaryRequest->cancelCb = nullptr;
mSummaryRequest->doneCb = nullptr;
mSummaryRequest->streamedResponseCb = nullptr;
}
if ( getPlugin() ) {
AIAssistantPlugin::AIAssistantConfig config;
config.partition = getSplitter()->getSplitPartition();
@@ -1168,28 +1178,27 @@ void LLMChatUI::generateChatName( bool isRenaming ) {
mSummaryRequest->doneCb = [this, isRenaming]( const LLMChatCompletionRequest& req,
Http::Response& response ) {
auto status = response.getStatus();
String oldSummary = std::move( mSummary );
runOnMainThread( [this, isRenaming, status, responseText = req.getResponse()] {
String oldSummary = std::move( mSummary );
if ( status == Http::Response::Ok ) {
mSummary = String::trim( req.getResponse() );
String::trimInPlace( mSummary, '\n' );
String::trimInPlace( mSummary, ' ' );
String::trimInPlace( mSummary, '"' );
} else {
// TODO: Implement generating a summary based on the user prompt (take the
// first few words)
mSummary = i18n( "untitled_conversation", "Untitled Conversation" );
}
if ( status == Http::Response::Ok ) {
mSummary = String::trim( responseText );
String::trimInPlace( mSummary, '\n' );
String::trimInPlace( mSummary, ' ' );
String::trimInPlace( mSummary, '"' );
} else {
// TODO: Implement generating a summary based on the user prompt (take the
// first few words)
mSummary = i18n( "untitled_conversation", "Untitled Conversation" );
}
if ( isRenaming ) {
String newSummary = std::move( mSummary );
mSummary = std::move( oldSummary );
runOnMainThread(
[this, newSummary = std::move( newSummary )] { renameChat( newSummary ); } );
} else
saveChat();
if ( isRenaming ) {
String newSummary = std::move( mSummary );
mSummary = std::move( oldSummary );
renameChat( newSummary );
} else
saveChat();
runOnMainThread( [this] {
updateTabTitle();
mSummaryRequest.reset();
} );
@@ -1258,15 +1267,17 @@ void LLMChatUI::doRequest() {
};
mRequest->cancelCb = [this, thinking, thinkingID, editor]( const LLMChatCompletionRequest& ) {
thinking->removeActionsByTag( thinkingID );
thinking->setVisible( false );
mChatStop->setVisible( false )->setEnabled( false );
mChatRun->setVisible( true )->setEnabled( true );
toggleEnableChats( true );
editor->setEnabled( true );
if ( editor->hasFocus() )
mChatInput->setFocus();
removeLastChat();
runOnMainThread( [this, thinking, thinkingID, editor] {
thinking->removeActionsByTag( thinkingID );
thinking->setVisible( false );
mChatStop->setVisible( false )->setEnabled( false );
mChatRun->setVisible( true )->setEnabled( true );
toggleEnableChats( true );
editor->setEnabled( true );
if ( editor->hasFocus() )
mChatInput->setFocus();
removeLastChat();
} );
};
mRequest->doneCb =

View File

@@ -120,6 +120,9 @@ LLMChatCompletionRequest::LLMChatCompletionRequest( const std::string& uri, cons
}
LLMChatCompletionRequest::~LLMChatCompletionRequest() {
cancelCb = nullptr;
doneCb = nullptr;
streamedResponseCb = nullptr;
cancel();
}

View File

@@ -2,6 +2,7 @@
#include "../debuggerclient.hpp"
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitreeview.hpp>
#include <eepp/window/engine.hpp>
namespace ecode {
@@ -73,6 +74,7 @@ VariablesModel::VariablesModel( ModelVariableNode::NodePtr rootNode, UISceneNode
mRootNode( rootNode ), mSceneNode( sceneNode ) {}
ModelIndex VariablesModel::index( int row, int column, const ModelIndex& parent ) const {
checkQueuedClear( parent );
if ( !mRootNode )
return ModelIndex();
@@ -115,6 +117,7 @@ ModelIndex VariablesModel::parentIndex( const ModelIndex& index ) const {
}
size_t VariablesModel::rowCount( const ModelIndex& index ) const {
checkQueuedClear( index );
ModelVariableNode* parentNode =
index.isValid() ? static_cast<ModelVariableNode*>( index.internalData() ) : mRootNode.get();
@@ -122,6 +125,7 @@ size_t VariablesModel::rowCount( const ModelIndex& index ) const {
}
bool VariablesModel::hasChildren( const ModelIndex& index ) const {
checkQueuedClear( index );
if ( !index.isValid() )
return !mRootNode->children.empty();
ModelVariableNode* node = static_cast<ModelVariableNode*>( index.internalData() );
@@ -165,9 +169,21 @@ Variant VariablesModel::data( const ModelIndex& index, ModelRole role ) const {
return EMPTY;
}
void VariablesModel::checkQueuedClear( const ModelIndex& index ) const {
if ( !index.isValid() && mQueuedClear && Engine::instance()->isMainThread() ) {
mChildMap.clear();
mQueuedClear = false;
}
}
void VariablesModel::invalidate( unsigned int flags ) {
if ( flags & Model::UpdateFlag::InvalidateAllIndexes ) {
mChildMap.clear();
if ( Engine::instance()->isMainThread() ) {
mChildMap.clear();
mQueuedClear = false;
} else {
mQueuedClear = true;
}
}
Model::invalidate( flags );
}
@@ -246,7 +262,7 @@ void VariablesHolder::upsertRootChild( Variable&& var ) {
auto newChild = std::make_shared<ModelVariableNode>( std::move( var ), mRootNode );
mNodeMap[newChild->var.variablesReference] = newChild;
mRootNode->children[i] = std::move( newChild );
mModel->invalidate( Model::UpdateFlag::DontInvalidateIndexes );
mModel->invalidate( Model::UpdateFlag::InvalidateAllIndexes );
return;
}
}

View File

@@ -125,7 +125,9 @@ class VariablesModel : public Model {
ModelVariableNode::NodePtr mRootNode;
mutable std::unordered_map<ModelVariableNode*, ModelVariableNode::NodePtr> mChildMap;
UISceneNode* mSceneNode;
mutable bool mQueuedClear{ false };
void checkQueuedClear( const ModelIndex& index ) const;
};
class VariablesHolder {