// Copyright (C) 2024-2026 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later #include "AgentListPane.hpp" #include "AgentListItem.hpp" #include "SettingsTheme.hpp" #include "SettingsUiBuilders.hpp" #include "TagFilterStrip.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace QodeAssist::Settings { AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent) : QFrame(parent) , m_factory(factory) { setFrameShape(QFrame::StyledPanel); m_filterEdit = new QLineEdit(this); m_filterEdit->setPlaceholderText(tr("Filter agents…")); m_filterEdit->setClearButtonEnabled(true); auto *filterRow = new QHBoxLayout; filterRow->setContentsMargins(6, 6, 6, 6); filterRow->addWidget(m_filterEdit, 1); m_filterHolder = new QWidget(this); m_filterHolder->setObjectName(QStringLiteral("FilterHolder")); m_filterHolder->setLayout(filterRow); m_filterHolder->setAutoFillBackground(true); m_tagStrip = new TagFilterStrip(this); m_listScroll = new QScrollArea(this); m_listScroll->setWidgetResizable(true); m_listScroll->setFrameShape(QFrame::NoFrame); m_listScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); auto *outer = new QVBoxLayout(this); outer->setContentsMargins(0, 0, 0, 0); outer->setSpacing(0); outer->addWidget(m_filterHolder); outer->addWidget(m_tagStrip); outer->addWidget(m_listScroll, 1); m_filterDebounce = new QTimer(this); m_filterDebounce->setSingleShot(true); m_filterDebounce->setInterval(100); connect(m_filterDebounce, &QTimer::timeout, this, &AgentListPane::rebuildList); connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) { m_filterDebounce->start(); }); connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this, [this](const QSet &) { rebuildList(); }, Qt::QueuedConnection); applyFilterHolderTheme(); } void AgentListPane::selectByName(const QString &name) { if (name.isEmpty()) return; setCurrentNameInternal(name, false); rebuildList(); for (auto *item : m_rows) { if (item->agentName() == name) { QTimer::singleShot(0, this, [this, item] { m_listScroll->ensureWidgetVisible(item, 0, 60); }); break; } } } void AgentListPane::refresh() { QMap counts; for (const auto *a : visibleAgents()) for (const QString &t : a->tags) counts[t] += 1; m_tagStrip->setAvailableTags(counts); rebuildList(); } void AgentListPane::changeEvent(QEvent *event) { QFrame::changeEvent(event); if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) applyFilterHolderTheme(); } void AgentListPane::applyFilterHolderTheme() { if (!m_filterHolder) return; const Theme theme = themeFor(palette()); m_filterHolder->setStyleSheet( QStringLiteral("QWidget#FilterHolder { background:%1;" " border-bottom:1px solid %2; }") .arg(theme.listHeaderBg, theme.rowSeparator)); } std::vector AgentListPane::visibleAgents() const { std::vector out; if (!m_factory) return out; for (const auto &a : m_factory->configs()) { if (a.hidden) continue; out.push_back(&a); } return out; } bool AgentListPane::matchesFilters(const AgentConfig &a, const QString &lowerFilter) const { if (!lowerFilter.isEmpty() && !(a.name + QLatin1Char(' ') + a.model).toLower().contains(lowerFilter)) return false; const QSet &active = m_tagStrip->activeTags(); for (const QString &t : active) if (!a.tags.contains(t)) return false; return true; } void AgentListPane::rebuildList() { const QString lowerFilter = m_filterEdit->text().trimmed().toLower(); std::vector userAgents; std::vector bundledAgents; for (const auto *a : visibleAgents()) { if (!matchesFilters(*a, lowerFilter)) continue; if (a->isUserSource()) userAgents.push_back(a); else bundledAgents.push_back(a); } auto byName = [](const AgentConfig *a, const AgentConfig *b) { return a->name.localeAwareCompare(b->name) < 0; }; std::sort(userAgents.begin(), userAgents.end(), byName); std::sort(bundledAgents.begin(), bundledAgents.end(), byName); QList newRows; auto *content = new QWidget; content->setAutoFillBackground(true); auto *contentLayout = new QVBoxLayout(content); contentLayout->setContentsMargins(0, 0, 0, 0); contentLayout->setSpacing(0); const QSet &activeTags = m_tagStrip->activeTags(); auto addAgents = [&](const std::vector &agents) { for (const AgentConfig *cfg : agents) { auto *item = new AgentListItem(*cfg, content); item->setSelected(cfg->name == m_currentName); item->setActiveTags(activeTags); connect(item, &AgentListItem::clicked, this, &AgentListPane::onRowClicked); connect(item, &AgentListItem::tagClicked, this, [this](const QString &) { refresh(); }, Qt::QueuedConnection); contentLayout->addWidget(item); newRows.append(item); } }; if (!userAgents.empty()) { contentLayout->addWidget(makeSectionHeader(tr("User"), content)); addAgents(userAgents); } if (!bundledAgents.empty()) { contentLayout->addWidget(makeSectionHeader(tr("Bundled"), content)); addAgents(bundledAgents); } if (newRows.isEmpty()) { auto *empty = new QLabel(tr("No agents match these filters."), content); empty->setAlignment(Qt::AlignCenter); empty->setContentsMargins(10, 16, 10, 16); QPalette ep = empty->palette(); ep.setColor(QPalette::WindowText, ep.color(QPalette::Mid)); empty->setPalette(ep); contentLayout->addWidget(empty); } contentLayout->addStretch(1); m_rows = newRows; m_listScroll->setWidget(content); const AgentConfig *current = m_currentName.isEmpty() || !m_factory ? nullptr : m_factory->configByName(m_currentName); if (!current && !m_rows.isEmpty()) { const QString fallback = m_rows.front()->agentName(); m_rows.front()->setSelected(true); setCurrentNameInternal(fallback, /*emitSignal*/ true); return; } emit currentAgentChanged(m_currentName); } void AgentListPane::onRowClicked(const QString &name) { setCurrentNameInternal(name, /*emitSignal*/ true); } void AgentListPane::setCurrentNameInternal(const QString &name, bool emitSignal) { if (name == m_currentName) return; m_currentName = name; for (auto *item : m_rows) item->setSelected(item->agentName() == name); if (emitSignal) emit currentAgentChanged(m_currentName); } } // namespace QodeAssist::Settings