// Copyright (C) 2026 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later #include "AgentSlotWidget.hpp" #include "SettingsTr.hpp" #include #include #include #include #include #include #include #include namespace QodeAssist::Settings { namespace { bool isDarkPalette(const QPalette &p) { return p.color(QPalette::Window).lightness() < 128; } QFont monoFont(int pixelSize) { QFont f; f.setFamilies({QStringLiteral("SF Mono"), QStringLiteral("Cascadia Code"), QStringLiteral("Consolas"), QStringLiteral("Liberation Mono"), QStringLiteral("Menlo"), QStringLiteral("Courier New")}); f.setStyleHint(QFont::Monospace); f.setPixelSize(pixelSize); return f; } struct Tone { QString cardBg; QString cardBd; QString textSoft; QString textMute; QString textFaint; }; Tone toneFor(bool dark) { return dark ? Tone{"#333333", "#4a4a4a", "#c2c2c2", "#9a9a9a", "#7a7a7a"} : Tone{"#f6f6f6", "#bdbdbd", "#3a3a3a", "#5a5a5a", "#8a8a8a"}; } struct PillTone { QString bg; QString fg; QString bd; }; PillTone pillTone(Pill::Kind kind, bool dark) { switch (kind) { case Pill::Template: return dark ? PillTone{"#2c3f5a", "#cfe1f7", "#4a6286"} : PillTone{"#dbe7f6", "#1f3f73", "#a8c1e0"}; case Pill::On: return dark ? PillTone{"#2f4530", "#bce0bd", "#4a6c4b"} : PillTone{"#dbe9d3", "#2c5a1c", "#a3bc97"}; case Pill::Off: return dark ? PillTone{"#3a3a3a", "#8a8a8a", "#4a4a4a"} : PillTone{"#ececec", "#7a7a7a", "#c8c8c8"}; case Pill::User: return dark ? PillTone{"#4a3f24", "#e6cd92", "#6a5a30"} : PillTone{"#f0e4cf", "#75541a", "#cdb98a"}; case Pill::Tag: return dark ? PillTone{"#2e2e3a", "#b9b9cf", "#46465a"} : PillTone{"#e7e7f2", "#46466e", "#c1c1d5"}; } return {}; } } // namespace // -- Pill -------------------------------------------------------------- Pill::Pill(Kind kind, const QString &text, QWidget *parent) : QLabel(text, parent) , m_kind(kind) { setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); setAlignment(Qt::AlignCenter); QFont f = font(); f.setPixelSize(11); setFont(f); applyTheme(); } void Pill::setKind(Kind kind) { if (m_kind == kind) return; m_kind = kind; applyTheme(); } void Pill::changeEvent(QEvent *event) { QLabel::changeEvent(event); if (m_inApplyTheme) return; if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) applyTheme(); } void Pill::applyTheme() { if (m_inApplyTheme) return; QScopedValueRollback guard(m_inApplyTheme, true); const auto t = pillTone(m_kind, isDarkPalette(palette())); setStyleSheet(QStringLiteral( "QLabel { background-color: %1; color: %2;" " border: 1px solid %3; padding: 1px 7px; }") .arg(t.bg, t.fg, t.bd)); } // -- AgentSlotWidget --------------------------------------------------- AgentSlotWidget::AgentSlotWidget(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_StyledBackground, true); setObjectName(QStringLiteral("AgentSlot")); m_name = new QLabel(this); QFont nameFont = m_name->font(); nameFont.setBold(true); m_name->setFont(nameFont); m_name->setTextInteractionFlags(Qt::TextSelectableByMouse); m_sourcePill = new Pill(Pill::User, {}, this); m_sourcePill->hide(); m_changeBtn = new QPushButton(Tr::tr("Change…"), this); m_changeBtn->setCursor(Qt::PointingHandCursor); connect(m_changeBtn, &QPushButton::clicked, this, &AgentSlotWidget::changeRequested); m_editBtn = new QPushButton(Tr::tr("Edit"), this); m_editBtn->setCursor(Qt::PointingHandCursor); m_editBtn->setToolTip(Tr::tr("Open this agent in the Agents settings page.")); connect(m_editBtn, &QPushButton::clicked, this, &AgentSlotWidget::editRequested); auto makeFieldLabel = [this]() { auto *l = new QLabel(this); l->setAlignment(Qt::AlignRight | Qt::AlignVCenter); return l; }; auto makeMonoValue = [this]() { auto *l = new QLabel(this); l->setFont(monoFont(11)); l->setTextInteractionFlags(Qt::TextSelectableByMouse); l->setMinimumWidth(0); l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); return l; }; auto makePlainValue = [this]() { auto *l = new QLabel(this); l->setTextInteractionFlags(Qt::TextSelectableByMouse); l->setMinimumWidth(0); l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); return l; }; m_modelLabel = makeFieldLabel(); m_modelLabel->setText(Tr::tr("Model")); m_modelValue = makeMonoValue(); m_urlLabel = makeFieldLabel(); m_urlLabel->setText(Tr::tr("Provider")); m_urlValue = makeMonoValue(); m_endpointLabel = makeFieldLabel(); m_endpointLabel->setText(Tr::tr("Endpoint")); m_endpointValue = makeMonoValue(); m_templateLabel = makeFieldLabel(); m_templateLabel->setText(Tr::tr("Template")); m_templateValue = makePlainValue(); m_description = new QLabel(this); m_description->setWordWrap(true); m_description->setTextInteractionFlags(Qt::TextSelectableByMouse); QFont descFont = m_description->font(); descFont.setItalic(true); m_description->setFont(descFont); m_thinkingPill = new Pill(Pill::Off, {}, this); m_toolsPill = new Pill(Pill::Off, {}, this); // Header row auto *nameRow = new QHBoxLayout; nameRow->setContentsMargins(0, 0, 0, 0); nameRow->setSpacing(6); nameRow->addWidget(m_name); nameRow->addWidget(m_sourcePill); nameRow->addStretch(1); auto *buttonsBox = new QHBoxLayout; buttonsBox->setContentsMargins(0, 0, 0, 0); buttonsBox->setSpacing(4); buttonsBox->addWidget(m_editBtn); buttonsBox->addWidget(m_changeBtn); auto *headerRow = new QHBoxLayout; headerRow->setContentsMargins(0, 0, 0, 0); headerRow->setSpacing(8); headerRow->addLayout(nameRow, 1); headerRow->addLayout(buttonsBox, 0); // Two-column field grid auto *grid = new QGridLayout; grid->setContentsMargins(0, 0, 0, 0); grid->setHorizontalSpacing(8); grid->setVerticalSpacing(2); grid->setColumnMinimumWidth(0, 62); grid->setColumnStretch(0, 0); grid->setColumnStretch(1, 1); grid->addWidget(m_modelLabel, 0, 0); grid->addWidget(m_modelValue, 0, 1); grid->addWidget(m_urlLabel, 1, 0); grid->addWidget(m_urlValue, 1, 1); grid->addWidget(m_endpointLabel, 2, 0); grid->addWidget(m_endpointValue, 2, 1); grid->addWidget(m_templateLabel, 3, 0); grid->addWidget(m_templateValue, 3, 1); auto *pillsRow = new QHBoxLayout; pillsRow->setContentsMargins(0, 0, 0, 0); pillsRow->setSpacing(4); pillsRow->addWidget(m_thinkingPill); pillsRow->addWidget(m_toolsPill); pillsRow->addStretch(1); auto *outer = new QVBoxLayout(this); outer->setContentsMargins(10, 10, 10, 10); outer->setSpacing(6); outer->addLayout(headerRow); outer->addLayout(grid); outer->addWidget(m_description); outer->addLayout(pillsRow); applyTheme(); clear(); } void AgentSlotWidget::changeEvent(QEvent *event) { QWidget::changeEvent(event); if (m_inApplyTheme) return; if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) applyTheme(); } void AgentSlotWidget::applyTheme() { if (m_inApplyTheme) return; QScopedValueRollback guard(m_inApplyTheme, true); const Tone t = toneFor(isDarkPalette(palette())); setStyleSheet(QStringLiteral( "#AgentSlot { background-color: %1; border: 1px solid %2; }") .arg(t.cardBg, t.cardBd)); auto applyColor = [](QLabel *label, const QString &color) { QPalette p = label->palette(); p.setColor(QPalette::WindowText, QColor(color)); label->setPalette(p); }; for (QLabel *l : {m_modelLabel, m_urlLabel, m_endpointLabel, m_templateLabel}) applyColor(l, t.textMute); applyColor(m_description, t.textSoft); } void AgentSlotWidget::setAgentConfig(const AgentConfig &cfg) { m_name->setText(cfg.name); if (cfg.isUserSource()) { m_sourcePill->setText(cfg.overridesBundled ? Tr::tr("User overrides bundled") : Tr::tr("User")); m_sourcePill->show(); } else { m_sourcePill->hide(); } m_modelValue->setText(cfg.model); m_modelValue->setToolTip(cfg.model); // The agent profile no longer carries a URL — the URL belongs to // the referenced provider instance and is editable on the Providers // settings page. Surface the instance name in this row instead. m_urlValue->setText(cfg.providerInstance); m_urlValue->setToolTip(cfg.providerInstance); m_endpointValue->setText(cfg.endpoint); m_endpointValue->setToolTip(cfg.endpoint); // Templates are now inline in the agent profile — no separate name to // surface. Keep the row but show '—' so the layout stays stable. m_templateLabel->hide(); m_templateValue->hide(); m_description->setText(cfg.description.isEmpty() ? Tr::tr("No description provided.") : cfg.description); const Tone t = toneFor(isDarkPalette(palette())); QPalette descPal = m_description->palette(); descPal.setColor(QPalette::WindowText, QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft)); m_description->setPalette(descPal); m_thinkingPill->setKind(cfg.enableThinking ? Pill::On : Pill::Off); m_thinkingPill->setText(cfg.enableThinking ? Tr::tr("thinking on") : Tr::tr("thinking off")); m_toolsPill->setKind(cfg.enableTools ? Pill::On : Pill::Off); m_toolsPill->setText(cfg.enableTools ? Tr::tr("tools on") : Tr::tr("tools off")); m_thinkingPill->show(); m_toolsPill->show(); m_editBtn->setEnabled(!cfg.name.isEmpty()); } void AgentSlotWidget::clear() { const Tone t = toneFor(isDarkPalette(palette())); m_name->setText(QStringLiteral("%2") .arg(t.textFaint, Tr::tr("(no agent selected)"))); m_sourcePill->hide(); for (QLabel *l : {m_modelValue, m_urlValue, m_endpointValue, m_templateValue}) { l->clear(); l->setToolTip({}); } m_description->setText({}); m_thinkingPill->hide(); m_toolsPill->hide(); m_editBtn->setEnabled(false); } } // namespace QodeAssist::Settings