mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 10:59:30 -04:00
362 lines
11 KiB
C++
362 lines
11 KiB
C++
// Copyright (C) 2026 Petr Mironychev
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "AgentSlotWidget.hpp"
|
|
|
|
#include "SettingsTr.hpp"
|
|
|
|
#include <QEvent>
|
|
#include <QFont>
|
|
#include <QGridLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QPalette>
|
|
#include <QPushButton>
|
|
#include <QScopedValueRollback>
|
|
#include <QVBoxLayout>
|
|
|
|
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<bool> 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<bool> 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("<i style=\"color:%1\">%2</i>")
|
|
.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
|