Skip to content

Commit 56c919b

Browse files
Merge pull request #2 from huydepzai121/master
feat(acp): collapse per-effort model variants, support separate reaso…
2 parents 1a90bf9 + 2f92efb commit 56c919b

1 file changed

Lines changed: 122 additions & 54 deletions

File tree

src/widgets/AcpSessionView.cpp

Lines changed: 122 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,60 @@ namespace {
7373
// when they reach this band) and for the programmatic scroll target.
7474
constexpr int kAtBottomEpsilonPx = 8;
7575

76+
bool isEffortConfigOption(const AcpProtocol::AcpConfigOption &opt)
77+
{
78+
const QString idLower = opt.id.toLower();
79+
const QString catLower = opt.category.toLower();
80+
const QString nameLower = opt.name.toLower();
81+
return idLower.contains(QLatin1String("effort"))
82+
|| idLower.contains(QLatin1String("reasoning"))
83+
|| catLower.contains(QLatin1String("thought"))
84+
|| catLower.contains(QLatin1String("reasoning"))
85+
|| nameLower.contains(QLatin1String("effort"))
86+
|| nameLower.contains(QLatin1String("reasoning"));
87+
}
88+
89+
bool isModelConfigOption(const AcpProtocol::AcpConfigOption &opt)
90+
{
91+
const QString idLower = opt.id.toLower();
92+
const QString catLower = opt.category.toLower();
93+
return idLower == QLatin1String("model")
94+
|| catLower == QLatin1String("model");
95+
}
96+
97+
QString baseModelId(const QString &modelId)
98+
{
99+
const int slash = modelId.indexOf(QLatin1Char('/'));
100+
return slash >= 0 ? modelId.left(slash) : modelId;
101+
}
102+
103+
QString stripEffortSuffix(const QString &label)
104+
{
105+
static const QRegularExpression re(
106+
QStringLiteral(R"(\s+\((low|medium|high|xhigh)\)$)"),
107+
QRegularExpression::CaseInsensitiveOption);
108+
const QRegularExpressionMatch match = re.match(label);
109+
return match.hasMatch() ? label.left(match.capturedStart()).trimmed() : label;
110+
}
111+
112+
const AcpProtocol::AcpConfigOption *findModelConfigOption(
113+
const QList<AcpProtocol::AcpConfigOption> &opts)
114+
{
115+
for (const auto &opt : opts) {
116+
if (isModelConfigOption(opt)) return &opt;
117+
}
118+
return nullptr;
119+
}
120+
121+
const AcpProtocol::AcpConfigOption *findEffortConfigOption(
122+
const QList<AcpProtocol::AcpConfigOption> &opts)
123+
{
124+
for (const auto &opt : opts) {
125+
if (isEffortConfigOption(opt)) return &opt;
126+
}
127+
return nullptr;
128+
}
129+
76130
// SVG icons that use stroke="currentColor" resolve to opaque black under Qt's
77131
// svg icon engine, so they vanish on dark backgrounds. Re-render the icon at
78132
// the sizes Qt is likely to ask for and tint each pixmap via SourceIn so the
@@ -1090,38 +1144,62 @@ void AcpSessionView::onMetadataChanged()
10901144
if (!m_model) return;
10911145
m_updatingSelectors = true;
10921146

1093-
// Models combo. Prefer the top-level models catalog; if the agent
1094-
// doesn't surface one, fall back to a `model` config option (Claude Code
1095-
// exposes the picker only via configOptions, never the models array).
1147+
const auto &configOpts = m_model->configOptions();
1148+
const AcpProtocol::AcpConfigOption *modelOpt = findModelConfigOption(configOpts);
1149+
const AcpProtocol::AcpConfigOption *effortOpt = findEffortConfigOption(configOpts);
1150+
1151+
// Models combo. Codex exposes base models via configOptions.model and
1152+
// reasoning effort separately (reasoning_effort). Its legacy models array
1153+
// still lists every model×effort pair ("gpt-5.5 (medium)", id "gpt-5.5/medium").
1154+
// Prefer the config option; when falling back to the models array, collapse
1155+
// to unique base models whenever effort is configured separately.
10961156
m_modelCombo->clear();
10971157
m_modelConfigOptionId.clear();
1098-
const auto &models = m_model->availableModels();
1099-
if (!models.isEmpty()) {
1100-
for (const auto &m : models) {
1101-
m_modelCombo->addItem(m.name.isEmpty() ? m.id : m.name, m.id);
1158+
1159+
if (modelOpt) {
1160+
m_modelConfigOptionId = modelOpt->id;
1161+
for (const auto &ch : modelOpt->options) {
1162+
const QString label = ch.name.isEmpty() ? ch.value : ch.name;
1163+
if (label.isEmpty()) continue;
1164+
if (m_modelCombo->findData(ch.value) >= 0) continue;
1165+
m_modelCombo->addItem(label, ch.value);
11021166
}
1103-
if (!m_model->currentModelId().isEmpty()) {
1104-
const int idx = m_modelCombo->findData(m_model->currentModelId());
1167+
const QString currentVal = modelOpt->currentValue.toString();
1168+
if (!currentVal.isEmpty()) {
1169+
const int idx = m_modelCombo->findData(currentVal);
11051170
if (idx >= 0) m_modelCombo->setCurrentIndex(idx);
11061171
}
1107-
} else {
1108-
for (const auto &opt : m_model->configOptions()) {
1109-
const QString idLower = opt.id.toLower();
1110-
const QString catLower = opt.category.toLower();
1111-
const bool matches = idLower == QLatin1String("model")
1112-
|| catLower == QLatin1String("model");
1113-
if (!matches) continue;
1114-
m_modelConfigOptionId = opt.id;
1115-
for (const auto &ch : opt.options) {
1116-
const QString label = ch.name.isEmpty() ? ch.value : ch.name;
1117-
if (!label.isEmpty()) m_modelCombo->addItem(label, ch.value);
1172+
}
1173+
1174+
if (m_modelCombo->count() == 0) {
1175+
const auto &models = m_model->availableModels();
1176+
bool collapseEffort = effortOpt != nullptr;
1177+
if (!collapseEffort) {
1178+
for (const auto &m : models) {
1179+
if (m.id.contains(QLatin1Char('/'))) {
1180+
collapseEffort = true;
1181+
break;
1182+
}
11181183
}
1119-
const QString currentVal = opt.currentValue.toString();
1120-
if (!currentVal.isEmpty()) {
1121-
const int idx = m_modelCombo->findData(currentVal);
1122-
if (idx >= 0) m_modelCombo->setCurrentIndex(idx);
1184+
}
1185+
1186+
for (const auto &m : models) {
1187+
QString id = m.id;
1188+
QString label = m.name.isEmpty() ? m.id : m.name;
1189+
if (collapseEffort) {
1190+
id = baseModelId(id);
1191+
label = stripEffortSuffix(label);
1192+
if (id.isEmpty()) continue;
11231193
}
1124-
break;
1194+
if (m_modelCombo->findData(id) >= 0) continue;
1195+
m_modelCombo->addItem(label, id);
1196+
}
1197+
1198+
QString currentId = m_model->currentModelId();
1199+
if (!currentId.isEmpty()) {
1200+
if (collapseEffort) currentId = baseModelId(currentId);
1201+
const int idx = m_modelCombo->findData(currentId);
1202+
if (idx >= 0) m_modelCombo->setCurrentIndex(idx);
11251203
}
11261204
}
11271205
m_modelCombo->setVisible(m_modelCombo->count() > 0);
@@ -1138,36 +1216,22 @@ void AcpSessionView::onMetadataChanged()
11381216
}
11391217
m_modeCombo->setVisible(!modes.isEmpty());
11401218

1141-
// Effort/reasoning config-option combo. Matches on id, category, or name
1142-
// so we pick up Claude Code's `effort` (category: "thought_level") as well
1143-
// as agents that surface a different label.
1219+
// Effort/reasoning combo (Claude Code, Codex reasoning_effort, etc).
11441220
m_effortCombo->clear();
11451221
m_effortConfigOptionId.clear();
1146-
const auto &configOpts = m_model->configOptions();
1147-
for (const auto &opt : configOpts) {
1148-
const QString idLower = opt.id.toLower();
1149-
const QString catLower = opt.category.toLower();
1150-
const QString nameLower = opt.name.toLower();
1151-
const bool matches = idLower.contains(QLatin1String("effort"))
1152-
|| idLower.contains(QLatin1String("reasoning"))
1153-
|| catLower.contains(QLatin1String("thought"))
1154-
|| catLower.contains(QLatin1String("reasoning"))
1155-
|| nameLower.contains(QLatin1String("effort"))
1156-
|| nameLower.contains(QLatin1String("reasoning"));
1157-
if (!matches) continue;
1158-
m_effortConfigOptionId = opt.id;
1159-
for (const auto &ch : opt.options) {
1222+
if (effortOpt) {
1223+
m_effortConfigOptionId = effortOpt->id;
1224+
for (const auto &ch : effortOpt->options) {
11601225
const QString label = ch.name.isEmpty() ? ch.value : ch.name;
1161-
if (!label.isEmpty()) {
1162-
m_effortCombo->addItem(label, ch.value);
1163-
}
1226+
if (label.isEmpty()) continue;
1227+
if (m_effortCombo->findData(ch.value) >= 0) continue;
1228+
m_effortCombo->addItem(label, ch.value);
11641229
}
1165-
const QString currentVal = opt.currentValue.toString();
1230+
const QString currentVal = effortOpt->currentValue.toString();
11661231
if (!currentVal.isEmpty()) {
11671232
const int idx = m_effortCombo->findData(currentVal);
11681233
if (idx >= 0) m_effortCombo->setCurrentIndex(idx);
11691234
}
1170-
break;
11711235
}
11721236
m_effortCombo->setVisible(m_effortCombo->count() > 0);
11731237

@@ -1498,8 +1562,6 @@ void AcpSessionView::onModelComboChanged(int index)
14981562
if (m_updatingSelectors || !m_connection || index < 0) return;
14991563
const QString id = m_modelCombo->itemData(index).toString();
15001564
if (id.isEmpty()) return;
1501-
// Config-option-backed picker (Claude Code) → session/set_config; the
1502-
// dedicated session/set_model channel only applies to the models array.
15031565
const QString prefKey = m_modelConfigOptionId.isEmpty()
15041566
? QStringLiteral("model") : m_modelConfigOptionId;
15051567
if (!m_modelConfigOptionId.isEmpty()) {
@@ -1512,6 +1574,7 @@ void AcpSessionView::onModelComboChanged(int index)
15121574
}
15131575
}
15141576

1577+
15151578
void AcpSessionView::onModeComboChanged(int index)
15161579
{
15171580
if (m_updatingSelectors || !m_connection || index < 0) return;
@@ -1536,6 +1599,7 @@ void AcpSessionView::onEffortComboChanged(int index)
15361599
}
15371600
}
15381601

1602+
15391603
void AcpSessionView::applySavedPreferences()
15401604
{
15411605
if (m_savedPrefsApplied) return;
@@ -1556,22 +1620,26 @@ void AcpSessionView::applySavedPreferences()
15561620
const QString modelPrefKey = m_modelConfigOptionId.isEmpty()
15571621
? QStringLiteral("model") : m_modelConfigOptionId;
15581622
const QString savedModel = m_registry->agentPreference(agentId, modelPrefKey);
1559-
if (!savedModel.isEmpty() && savedModel != m_model->currentModelId()) {
1623+
const QString savedModelBase = baseModelId(savedModel);
1624+
const QString currentModelBase = baseModelId(m_model->currentModelId());
1625+
if (!savedModel.isEmpty() && savedModelBase != currentModelBase) {
15601626
if (!m_modelConfigOptionId.isEmpty()) {
15611627
for (const auto &opt : m_model->configOptions()) {
15621628
if (opt.id != m_modelConfigOptionId) continue;
15631629
for (const auto &ch : opt.options) {
1564-
if (ch.value == savedModel) {
1565-
m_connection->setConfigOption(m_modelConfigOptionId, savedModel);
1630+
if (ch.value == savedModel || ch.value == savedModelBase) {
1631+
m_connection->setConfigOption(m_modelConfigOptionId, ch.value);
15661632
break;
15671633
}
15681634
}
15691635
break;
15701636
}
15711637
} else {
15721638
for (const auto &m : m_model->availableModels()) {
1573-
if (m.id == savedModel) {
1574-
m_connection->setModel(savedModel);
1639+
if (m.id == savedModel || baseModelId(m.id) == savedModelBase) {
1640+
m_connection->setModel(m.id.contains(QLatin1Char('/'))
1641+
? baseModelId(m.id)
1642+
: m.id);
15751643
break;
15761644
}
15771645
}

0 commit comments

Comments
 (0)