Add resource_link support.

Agent Config WIP.
This commit is contained in:
Martín Lucas Golini
2026-03-22 21:05:59 -03:00
parent 00739a7b4e
commit 0e7909a2f0
10 changed files with 298 additions and 5 deletions

Binary file not shown.

View File

@@ -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 ) );

View File

@@ -241,6 +241,63 @@ void ACPClient::loadSession(
} );
}
void ACPClient::setConfigOption(
const SetConfigOptionRequest& req,
const std::function<void( const SetConfigOptionResponse&,
const std::optional<ResponseError>& )>& 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<void( const ListSessionsResponse&, const std::optional<ResponseError>& )>&

View File

@@ -52,6 +52,9 @@ class ACPClient {
void loadSession( const LoadSessionRequest& req,
const std::function<void( const LoadSessionResponse&,
const std::optional<ResponseError>& )>& cb );
void setConfigOption( const SetConfigOptionRequest& req,
const std::function<void( const SetConfigOptionResponse&,
const std::optional<ResponseError>& )>& cb );
void listSessions( const ListSessionsRequest& req,
const std::function<void( const ListSessionsResponse&,
const std::optional<ResponseError>& )>& cb );
@@ -102,6 +105,7 @@ class ACPClient {
std::map<IdType, JsonReplyHandler> mHandlers;
std::string mReceiveBuffer;
bool mLegacyConfigOnly{ false };
void readStdOut( const char* bytes, size_t n );
void readStdErr( const char* bytes, size_t n );

View File

@@ -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 ) {

View File

@@ -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<int>();

View File

@@ -39,6 +39,7 @@ bool AgentSession::start( const std::function<void( bool )>& 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<ResponseError>& 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 );
};

View File

@@ -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<ThreadPool> mThreadPool;
std::unique_ptr<ACPClient> mClient;
std::string mSessionId;
json mConfigOptions;
bool mIsPrompting{ false };
struct TermData {

View File

@@ -532,6 +532,7 @@ DropDownList.role_ui {
<PushButton id="llm_more" class="llm_button" tooltip="@string(more_options, More Options)" icon="icon(more-fill, 14dp)" min-width="32dp" />
<PushButton class="model_ui" lw="0" lw8="1" lh="mp" margin-left="4dp" margin-right="4dp" tooltip="@string(select_model, Select Model)" />
<PushButton class="agent_ui" lw="0" lw8="1" lh="mp" margin-left="4dp" margin-right="4dp" tooltip="@string(select_agent, Select Agent)" visible="false" />
<PushButton class="agent_config_ui" tooltip="@string(agent_config, Agent Config)" icon="icon(agent, 14dp)" min-width="32dp" margin-right="4dp" visible="false" />
<SelectButton id="llm_agent_mode" class="llm_button" tooltip="@string(toggle_agent_mode, Toggle Agent Mode)" icon="icon(robot-2, 14dp)" min-width="32dp" margin-right="4dp" select-on-click="true" />
<PushButton id="llm_settings_but" class="llm_button" text="@string(settings, Settings)" tooltip="@string(settings, Settings)" icon="icon(settings, 14dp)" min-width="32dp" margin-right="4dp" />
<PushButton id="llm_add_chat" class="llm_button" text="@string(add, Add)" tooltip="@string(add_message, Add Message)" icon="icon(add, 15dp)" min-width="32dp" margin-right="4dp" />
@@ -598,6 +599,8 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) :
mModelBtn->onClick( [this]( auto ) { execute( "ai-select-model" ); } );
mAgentBtn = findByClass<UIPushButton>( "agent_ui" );
mAgentBtn->onClick( [this]( auto ) { execute( "ai-select-agent" ); } );
mAgentConfigBtn = findByClass<UIPushButton>( "agent_config_ui" );
mAgentConfigBtn->onClick( [this]( auto ) { showAgentConfigMenu(); } );
mChatAgentMode = find<UISelectButton>( "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>();
std::string currentVal =
opt.contains( "currentValue" ) ? opt["currentValue"].get<std::string>() : "";
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>();
std::string subName = subopt["name"].get<std::string>();
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<UIMenuItem>();
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<acp::ResponseError>& 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<std::string>(), 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<UIVLinearLayoutCommandExecuter>( "llm_chat_select_agent" );
mLocateAgentInput = findByClass<UITextInput>( "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;

View File

@@ -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<TextDocument> cdoc );
void replaceFileLinksToContents( std::string& text );