@@ -73,6 +73,60 @@ namespace {
7373// when they reach this band) and for the programmatic scroll target.
7474constexpr 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+
15151578void 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+
15391603void 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