From e38993883ab52878b85212607f3528522c5c553a Mon Sep 17 00:00:00 2001 From: Christian Kandeler Date: Fri, 6 Jun 2025 14:08:47 +0200 Subject: [PATCH] ProjectExplorer: Let users query a language model ... about entries in the issues pane. This is a simple proof of concept that just takes the first defined language model, provides it with a hard-coded prompt derived from the respective issue, and dumps the output into the general messages pane. Change-Id: I9a8c27be3ef16c75d007a74790e7ea0b1e6f1f04 Reviewed-by: hjk --- .../coreplugin/customlanguagemodels.cpp | 29 ++----- src/plugins/coreplugin/customlanguagemodels.h | 3 +- src/plugins/projectexplorer/taskhandlers.cpp | 81 ++++++++++++++++++- 3 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/plugins/coreplugin/customlanguagemodels.cpp b/src/plugins/coreplugin/customlanguagemodels.cpp index a84c0781b9a..b77417f9aca 100644 --- a/src/plugins/coreplugin/customlanguagemodels.cpp +++ b/src/plugins/coreplugin/customlanguagemodels.cpp @@ -30,7 +30,7 @@ public: static const CustomLanguageModel &fromBaseAspect(const std::shared_ptr &aspect); CustomLanguageModel(); - CommandLine commandLine(const QString &prompt); + CommandLine commandLine(); StringAspect name{this}; FilePathAspect executable{this}; @@ -73,16 +73,6 @@ public: LanguageModelsListModel listModel{models}; }; -class PromptMacroExpander : public MacroExpander -{ -public: - PromptMacroExpander(const QString &prompt) - { - registerVariable("Prompt", Tr::tr("The prompt to query the model with"), - [prompt] { return prompt; }); - } -}; - CustomLanguageModels &customLanguageModels() { static CustomLanguageModels models; @@ -101,14 +91,9 @@ public: } }; -CommandLine CustomLanguageModel::commandLine(const QString &prompt) +CommandLine CustomLanguageModel::commandLine() { - MacroExpander * const exp = arguments.macroExpander(); - PromptMacroExpander expander(prompt); - arguments.setMacroExpander(&expander); - CommandLine cmdLine(executable.effectiveBinary(), arguments.expandedValue(), CommandLine::Raw); - arguments.setMacroExpander(exp); - return cmdLine; + return CommandLine(executable.effectiveBinary(), arguments.expandedValue(), CommandLine::Raw); } CustomLanguageModel &CustomLanguageModel::fromBaseAspect(BaseAspect &aspect) @@ -136,12 +121,8 @@ CustomLanguageModel::CustomLanguageModel() arguments.setSettingsKey("Arguments"); arguments.setDisplayName(Tr::tr("Arguments")); arguments.setLabelText(Tr::tr("Arguments:")); - arguments.setDefaultValue({"%{Prompt}"}); arguments.setDisplayStyle(StringAspect::LineEditDisplay); - static PromptMacroExpander expander({}); - arguments.setMacroExpander(&expander); - using namespace Layouting; setLayouter([this] { return Form{name, br, executable, br, arguments}; }); } @@ -271,11 +252,11 @@ QStringList availableLanguageModels() }); } -CommandLine commandLineForLanguageModel(const QString &model, const QString &prompt) +CommandLine commandLineForLanguageModel(const QString &model) { for (const auto &l = customLanguageModels().models.items(); const auto &i : l) { if (auto &m = CustomLanguageModel::fromBaseAspect(*i); m.name() == model) - return m.commandLine(prompt); + return m.commandLine(); } return {}; } diff --git a/src/plugins/coreplugin/customlanguagemodels.h b/src/plugins/coreplugin/customlanguagemodels.h index 7c1e2560291..55100eeba04 100644 --- a/src/plugins/coreplugin/customlanguagemodels.h +++ b/src/plugins/coreplugin/customlanguagemodels.h @@ -9,8 +9,7 @@ namespace Core { CORE_EXPORT QStringList availableLanguageModels(); -CORE_EXPORT Utils::CommandLine commandLineForLanguageModel( - const QString &model, const QString &prompt); +CORE_EXPORT Utils::CommandLine commandLineForLanguageModel(const QString &model); namespace Internal { diff --git a/src/plugins/projectexplorer/taskhandlers.cpp b/src/plugins/projectexplorer/taskhandlers.cpp index 4f4f470e3b8..1dabe4c975b 100644 --- a/src/plugins/projectexplorer/taskhandlers.cpp +++ b/src/plugins/projectexplorer/taskhandlers.cpp @@ -8,13 +8,19 @@ #include "taskhub.h" #include +#include #include #include #include +#include #include +#include #include +#include + using namespace Core; +using namespace Utils; namespace ProjectExplorer::Internal { namespace { @@ -22,7 +28,7 @@ namespace { class ConfigTaskHandler : public ITaskHandler { public: - ConfigTaskHandler(const Task &pattern, Utils::Id page) + ConfigTaskHandler(const Task &pattern, Id page) : m_pattern(pattern) , m_targetPage(page) {} @@ -48,7 +54,7 @@ private: private: const Task m_pattern; - const Utils::Id m_targetPage; + const Id m_targetPage; }; class CopyTaskHandler : public ITaskHandler @@ -77,10 +83,10 @@ private: lines << task.file.toUserOutput() + ':' + QString::number(task.line) + ": " + type + task.description(); } - Utils::setClipboardAndSelection(lines.join('\n')); + setClipboardAndSelection(lines.join('\n')); } - Utils::Id actionManagerId() const override { return Utils::Id(Core::Constants::COPY); } + Id actionManagerId() const override { return Id(Core::Constants::COPY); } QAction *createAction(QObject *parent) const override { return new QAction(parent); } }; @@ -165,6 +171,72 @@ class VcsAnnotateTaskHandler : public ITaskHandler } }; +// FIXME: There should be one handler per LLM, but the ITaskHandler infrastructure is +// currently static. Alternatively, we could somehow multiplex from here, perhaps via a submenu. +class ExplainWithAiHandler : public ITaskHandler +{ + bool canHandle(const Task &task) const override + { + Q_UNUSED(task) + return !availableLanguageModels().isEmpty(); + } + + void handle(const Task &task) override + { + const QStringList llms = availableLanguageModels(); + QTC_ASSERT(!llms.isEmpty(), return); + + QString prompt; + if (task.origin.isEmpty()) + prompt += "A software tool has emitted a diagnostic. "; + else + prompt += QString("The tool \"%1\" has emitted a diagnostic. ").arg(task.origin); + prompt += "Please explain what it is about. " + "Be as concise and concrete as possible and try to name the root cause." + "If you don't know the answer, just say so." + "If possible, also provide a solution. " + "Do not think for more than a minute. " + "Here is the error: ###%1##"; + prompt = prompt.arg(task.description()); + if (task.file.exists()) { + if (const auto contents = task.file.fileContents()) { + prompt.append('\n').append( + "Ideally, provide your solution in the form of a diff." + "Here are the contents of the file that the tool complained about: ###%1###." + "The path to the file is ###%2###."); + prompt = prompt.arg(QString::fromUtf8(*contents), task.file.toUserOutput()); + } + } + const auto process = new Process; + process->setCommand(commandLineForLanguageModel(llms.first())); + process->setProcessMode(ProcessMode::Writer); + process->setTextChannelMode(Channel::Output, TextChannelMode::MultiLine); + process->setTextChannelMode(Channel::Error, TextChannelMode::MultiLine); + connect(process, &Process::textOnStandardOutput, + [](const QString &text) { MessageManager::writeFlashing(text); }); + connect(process, &Process::textOnStandardError, + [](const QString &text) { MessageManager::writeFlashing(text); }); + connect(process, &Process::done, [process] { + MessageManager::writeSilently(process->exitMessage()); + process->deleteLater(); + }); + connect(process, &Process::started, [process, prompt] { + process->write(prompt); + process->closeWriteChannel(); + }); + QTimer::singleShot(60000, process, [process] { process->kill(); }); + MessageManager::writeDisrupting(Tr::tr("Querying LLM...")); + process->start(); + } + + QAction *createAction(QObject *parent) const override + { + const auto action = new QAction(Tr::tr("Get help from AI"), parent); + action->setToolTip(Tr::tr("Ask an AI to help with this issue.")); + return action; + } +}; + } // namespace void setupTaskHandlers() @@ -175,6 +247,7 @@ void setupTaskHandlers() static const RemoveTaskHandler removeTaskHandler; static const ShowInEditorTaskHandler showInEditorTaskHandler; static const VcsAnnotateTaskHandler vcsAnnotateTaskHandler; + static const ExplainWithAiHandler explainWithAiHandler; } } // namespace ProjectExplorer::Internal