Skip to content

Commit 89bea39

Browse files
fix(acp): restore omp effort after its model, exact model ids, omp advisor
- omp lists thinking levels per model: the saved effort ("high") is not offered by the default model, so it was skipped and the "off" the agent reported was saved as the default; every new chat started Off. A restored effort the catalog lacks now waits for the restored model's switch (m_pendingEffort), is set once offered, and the reset value is never saved meanwhile. - Config-option model restore matches the saved id exactly before falling back to Codex's "model/effort" base, and compares with the option's own value: pi-acp/omp ids are "provider/model", so a saved model sharing the default's provider was taken for it and never restored. - /advisor follows the advertised hint: omp's "[on|off|status|...]" gets "/advisor on|off" instead of "/advisor opus", and is not re-sent on a model change; typed status/dump/configure no longer tick the toggle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a61782d commit 89bea39

6 files changed

Lines changed: 311 additions & 33 deletions

File tree

‎src/AcpProtocol.cpp‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,15 +978,45 @@ bool commandsIncludeGoal(const QList<AcpCommandInfo> &commands)
978978
return false;
979979
}
980980

981-
bool commandsIncludeAdvisor(const QList<AcpCommandInfo> &commands)
981+
namespace {
982+
983+
const AcpCommandInfo *findAdvisorCommand(const QList<AcpCommandInfo> &commands)
982984
{
983985
for (const AcpCommandInfo &cmd : commands) {
984986
QStringView v{cmd.name};
985987
v = v.trimmed();
986988
while (!v.isEmpty() && v.front() == QLatin1Char('/'))
987989
v = v.mid(1);
988990
if (v.compare(QLatin1String("advisor"), Qt::CaseInsensitive) == 0)
991+
return &cmd;
992+
}
993+
return nullptr;
994+
}
995+
996+
} // namespace
997+
998+
bool commandsIncludeAdvisor(const QList<AcpCommandInfo> &commands)
999+
{
1000+
return findAdvisorCommand(commands) != nullptr;
1001+
}
1002+
1003+
bool advisorIsToggle(const QList<AcpCommandInfo> &commands)
1004+
{
1005+
const AcpCommandInfo *cmd = findAdvisorCommand(commands);
1006+
if (!cmd) return false;
1007+
// "[on|off|status|dump [raw]|configure]": alternatives split by | [ ] and spaces.
1008+
const QStringView hint{cmd->inputHint};
1009+
qsizetype start = -1;
1010+
for (qsizetype i = 0; i <= hint.size(); ++i) {
1011+
const bool sep = i == hint.size() || hint[i] == QLatin1Char('|') || hint[i] == QLatin1Char('[')
1012+
|| hint[i] == QLatin1Char(']') || hint[i].isSpace();
1013+
if (!sep) {
1014+
if (start < 0) start = i;
1015+
continue;
1016+
}
1017+
if (start >= 0 && hint.mid(start, i - start).compare(QLatin1String("on"), Qt::CaseInsensitive) == 0)
9891018
return true;
1019+
start = -1;
9901020
}
9911021
return false;
9921022
}
@@ -1016,6 +1046,14 @@ QString advisorCommandText(bool on, const QString &modelValue, const QString &mo
10161046
: QStringLiteral("/advisor off");
10171047
}
10181048

1049+
QString advisorCommandText(bool on, const QList<AcpCommandInfo> &commands,
1050+
const QString &modelValue, const QString &modelLabel)
1051+
{
1052+
if (advisorIsToggle(commands))
1053+
return on ? QStringLiteral("/advisor on") : QStringLiteral("/advisor off");
1054+
return advisorCommandText(on, modelValue, modelLabel);
1055+
}
1056+
10191057
std::optional<bool> parseAdvisorCommand(const QString &text)
10201058
{
10211059
QStringView v = QStringView{text}.trimmed();
@@ -1029,6 +1067,19 @@ std::optional<bool> parseAdvisorCommand(const QString &text)
10291067
v = v.trimmed();
10301068
if (v.isEmpty())
10311069
return std::nullopt;
1070+
// omp's queries and setup leave the advisor as it is.
1071+
QStringView verb = v;
1072+
for (qsizetype i = 0; i < v.size(); ++i) {
1073+
if (v[i].isSpace()) {
1074+
verb = v.left(i);
1075+
break;
1076+
}
1077+
}
1078+
for (const QLatin1String query : {QLatin1String("status"), QLatin1String("dump"),
1079+
QLatin1String("configure")}) {
1080+
if (verb.compare(query, Qt::CaseInsensitive) == 0)
1081+
return std::nullopt;
1082+
}
10321083
return v.compare(QLatin1String("off"), Qt::CaseInsensitive) != 0;
10331084
}
10341085

‎src/AcpProtocol.h‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,8 +419,15 @@ bool commandsIncludeAdvisor(const QList<AcpCommandInfo> &commands);
419419
QString advisorModelFor(const QString &modelValue, const QString &modelLabel = {});
420420
// "/advisor <advisor model>" when on, "/advisor off" when off.
421421
QString advisorCommandText(bool on, const QString &modelValue, const QString &modelLabel = {});
422-
// Reads a typed `/advisor` command: "off" -> false, a model argument -> true.
423-
// Bare `/advisor` (status query) and any other text -> nullopt.
422+
// True when the advertised `/advisor` is an on/off switch rather than a model
423+
// pick: its hint lists "on" (omp: "[on|off|status|dump [raw]|configure]").
424+
bool advisorIsToggle(const QList<AcpCommandInfo> &commands);
425+
// The command in the form the agent advertises: "/advisor on" / "/advisor off"
426+
// for a toggle, else advisorCommandText(on, modelValue, modelLabel).
427+
QString advisorCommandText(bool on, const QList<AcpCommandInfo> &commands,
428+
const QString &modelValue, const QString &modelLabel = {});
429+
// Reads a typed `/advisor` command: "off" -> false, "on" or a model -> true.
430+
// Bare `/advisor`, omp's status/dump/configure and any other text -> nullopt.
424431
std::optional<bool> parseAdvisorCommand(const QString &text);
425432
// Prepend `goal` when missing. O(n) over a few dozen commands; no heap if present.
426433
void ensureHostGoalCommand(QList<AcpCommandInfo> &commands);

‎src/widgets/AcpSessionView.cpp‎

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,7 @@ void AcpSessionView::rebind(AcpSessionModel *model, AcpConnection *connection)
11441144
m_model = model;
11451145
m_connection = connection;
11461146
m_savedPrefsApplied = false;
1147+
m_pendingEffort = {};
11471148
// The conversation is gone: a restarted session is a draft again.
11481149
m_draft = true;
11491150
m_carry = {};
@@ -1288,6 +1289,7 @@ void AcpSessionView::rebindConnection(AcpConnection *connection)
12881289
disconnect(m_registry, nullptr, this, nullptr);
12891290
m_connection = connection;
12901291
m_savedPrefsApplied = false;
1292+
m_pendingEffort = {};
12911293
++m_scheduledRev;
12921294
m_parked = {};
12931295
m_resumeBlocked = false;
@@ -2653,6 +2655,7 @@ void AcpSessionView::onMetadataChanged()
26532655
// Once the agent's catalogs have arrived, push any saved per-agent
26542656
// selections back into the session (one-shot per session).
26552657
applySavedPreferences();
2658+
applyPendingEffort();
26562659

26572660
// After the initial restore, persist any agent-side config changes
26582661
// (e.g. Codex CLI's config_option_update for model/mode/effort) so the
@@ -2674,7 +2677,7 @@ void AcpSessionView::onMetadataChanged()
26742677
if (!curMode.isEmpty())
26752678
m_registry->setAgentPreference(agentId, QStringLiteral("mode"), curMode);
26762679

2677-
if (!m_effortConfigOptionId.isEmpty()) {
2680+
if (!m_effortConfigOptionId.isEmpty() && m_pendingEffort.effort.isEmpty()) {
26782681
for (const auto &opt : m_model->configOptions()) {
26792682
if (opt.id == m_effortConfigOptionId) {
26802683
const QString val = opt.currentValue.toString();
@@ -2708,7 +2711,8 @@ void AcpSessionView::sendAdvisorCommand(bool on)
27082711
if (!m_connection || !m_model || !m_modelCombo) return;
27092712
const int idx = m_modelCombo->currentIndex();
27102713
submitSlashCommand(AcpProtocol::advisorCommandText(
2711-
on, m_modelCombo->itemData(idx).toString(), m_modelCombo->itemText(idx)));
2714+
on, m_model->availableCommands(), m_modelCombo->itemData(idx).toString(),
2715+
m_modelCombo->itemText(idx)));
27122716
}
27132717

27142718
// Same routing as typing the command and pressing Send: queued while a turn
@@ -3748,6 +3752,7 @@ void AcpSessionView::onModelComboChanged(int index)
37483752
if (id.isEmpty()) return;
37493753
const QString prefKey = m_modelConfigOptionId.isEmpty()
37503754
? QStringLiteral("model") : m_modelConfigOptionId;
3755+
m_pendingEffort = {}; // the user's pick replaces the restore
37513756
if (!m_connection->isSessionReady()) {
37523757
// Agent not started yet (restored session): applied once it is.
37533758
m_carry.model = id;
@@ -3764,8 +3769,10 @@ void AcpSessionView::onModelComboChanged(int index)
37643769
if (m_registry && m_draft) {
37653770
m_registry->setAgentPreference(m_connection->definition().id, prefKey, id);
37663771
}
3767-
// The advisor tracks the selected model's level: re-point it.
3768-
if (m_advisorBtn && m_advisorBtn->isVisible() && m_advisorBtn->isChecked())
3772+
// The advisor tracks the selected model's level: re-point it. An on/off
3773+
// advisor (omp) names no model, so there is nothing to re-point.
3774+
if (m_advisorBtn && m_advisorBtn->isVisible() && m_advisorBtn->isChecked() && m_model
3775+
&& !AcpProtocol::advisorIsToggle(m_model->availableCommands()))
37693776
sendAdvisorCommand(true);
37703777
}
37713778

@@ -3792,6 +3799,7 @@ void AcpSessionView::onEffortComboChanged(int index)
37923799
if (m_updatingSelectors || !m_connection || index < 0) return;
37933800
if (m_effortConfigOptionId.isEmpty()) return;
37943801
const QString val = m_effortCombo->itemData(index).toString();
3802+
m_pendingEffort = {};
37953803
if (m_connection->isSessionReady()) {
37963804
m_connection->setConfigOption(m_effortConfigOptionId, val);
37973805
} else {
@@ -3846,26 +3854,32 @@ void AcpSessionView::applySavedPreferences()
38463854
const QString savedModel = want.model;
38473855
const QString savedModelBase = baseModelId(savedModel);
38483856
const QString currentModelBase = baseModelId(m_model->currentModelId());
3849-
if (!savedModel.isEmpty() && savedModelBase != currentModelBase) {
3850-
if (!m_modelConfigOptionId.isEmpty()) {
3851-
for (const auto &opt : m_model->configOptions()) {
3852-
if (opt.id != m_modelConfigOptionId) continue;
3853-
for (const auto &ch : opt.options) {
3854-
if (ch.value == savedModel || ch.value == savedModelBase) {
3855-
m_connection->setConfigOption(m_modelConfigOptionId, ch.value);
3856-
break;
3857-
}
3858-
}
3859-
break;
3857+
QString modelSent;
3858+
if (!savedModel.isEmpty() && !m_modelConfigOptionId.isEmpty()) {
3859+
// A choice equal to the saved id wins: omp ids are "provider/model",
3860+
// where the part before the slash is no model. Only a saved Codex
3861+
// "model/effort" id falls back to its base model.
3862+
for (const auto &opt : m_model->configOptions()) {
3863+
if (opt.id != m_modelConfigOptionId) continue;
3864+
const auto has = [&opt](const QString &v) {
3865+
return std::any_of(opt.options.cbegin(), opt.options.cend(),
3866+
[&v](const auto &ch) { return ch.value == v; });
3867+
};
3868+
const QString target = has(savedModel) ? savedModel
3869+
: has(savedModelBase) ? savedModelBase
3870+
: QString();
3871+
if (!target.isEmpty() && target != opt.currentValue.toString()) {
3872+
modelSent = target;
3873+
m_connection->setConfigOption(m_modelConfigOptionId, target);
38603874
}
3861-
} else {
3862-
for (const auto &m : m_model->availableModels()) {
3863-
if (m.id == savedModel || baseModelId(m.id) == savedModelBase) {
3864-
m_connection->setModel(m.id.contains(QLatin1Char('/'))
3865-
? baseModelId(m.id)
3866-
: m.id);
3867-
break;
3868-
}
3875+
break;
3876+
}
3877+
} else if (!savedModel.isEmpty() && savedModelBase != currentModelBase) {
3878+
for (const auto &m : m_model->availableModels()) {
3879+
if (m.id == savedModel || baseModelId(m.id) == savedModelBase) {
3880+
modelSent = m.id.contains(QLatin1Char('/')) ? baseModelId(m.id) : m.id;
3881+
m_connection->setModel(modelSent);
3882+
break;
38693883
}
38703884
}
38713885
}
@@ -3881,25 +3895,56 @@ void AcpSessionView::applySavedPreferences()
38813895
}
38823896
}
38833897

3884-
// Effort / reasoning level: keyed by the matched config-option id.
3898+
// Effort / reasoning level: keyed by the matched config-option id. A level
3899+
// the current model lacks may be the restored model's: wait for its switch.
38853900
if (!m_effortConfigOptionId.isEmpty()) {
38863901
const QString savedEffort = want.effort;
38873902
if (!savedEffort.isEmpty()) {
38883903
for (const auto &opt : m_model->configOptions()) {
38893904
if (opt.id != m_effortConfigOptionId) continue;
38903905
if (opt.currentValue.toString() == savedEffort) break;
3891-
for (const auto &ch : opt.options) {
3892-
if (ch.value == savedEffort) {
3893-
m_connection->setConfigOption(m_effortConfigOptionId, savedEffort);
3894-
break;
3895-
}
3896-
}
3906+
const bool offered = std::any_of(opt.options.cbegin(), opt.options.cend(),
3907+
[&](const auto &ch) { return ch.value == savedEffort; });
3908+
if (offered)
3909+
m_connection->setConfigOption(m_effortConfigOptionId, savedEffort);
3910+
else if (!modelSent.isEmpty())
3911+
m_pendingEffort = {savedEffort, modelSent, false};
38973912
break;
38983913
}
38993914
}
39003915
}
39013916
}
39023917

3918+
void AcpSessionView::applyPendingEffort()
3919+
{
3920+
if (m_pendingEffort.effort.isEmpty() || !m_connection || !m_model) return;
3921+
const QString current = m_model->currentModelId();
3922+
const bool modelLanded = current == m_pendingEffort.model
3923+
|| baseModelId(current) == m_pendingEffort.model;
3924+
const auto &opts = m_model->configOptions();
3925+
const auto opt = std::find_if(opts.cbegin(), opts.cend(), [this](const auto &o) {
3926+
return !m_effortConfigOptionId.isEmpty() && o.id == m_effortConfigOptionId;
3927+
});
3928+
if (opt == opts.cend()) {
3929+
if (modelLanded) m_pendingEffort = {};
3930+
return;
3931+
}
3932+
if (opt->currentValue.toString() == m_pendingEffort.effort) {
3933+
m_pendingEffort = {}; // landed: what the agent reports is the default again
3934+
return;
3935+
}
3936+
if (m_pendingEffort.sent) return;
3937+
const bool offered = std::any_of(opt->options.cbegin(), opt->options.cend(), [this](const auto &ch) {
3938+
return ch.value == m_pendingEffort.effort;
3939+
});
3940+
if (offered) {
3941+
m_connection->setConfigOption(m_effortConfigOptionId, m_pendingEffort.effort);
3942+
m_pendingEffort.sent = true;
3943+
} else if (modelLanded) {
3944+
m_pendingEffort = {}; // the restored model does not offer it either
3945+
}
3946+
}
3947+
39033948
void AcpSessionView::onJumpToBottomClicked()
39043949
{
39053950
if (!m_scroll) return;

‎src/widgets/AcpSessionView.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,8 @@ private slots:
419419
// continues on a new agent process. Called once per session after
420420
// metadata first populates the available catalogs.
421421
void applySavedPreferences();
422+
// Sets m_pendingEffort once the agent's catalog offers it.
423+
void applyPendingEffort();
422424
// Remember the model's current model / mode / effort for applySavedPreferences.
423425
void captureSelection();
424426
// The model and effort the agent reports now.
@@ -638,6 +640,18 @@ private slots:
638640
// The session's own selection to re-apply on its next agent process
639641
// (Switch agent, restored session); consumed by applySavedPreferences.
640642
Selection m_carry;
643+
// A restored effort the agent does not offer yet: omp lists thinking levels
644+
// per model, so it waits for the restored model's switch to land, then is
645+
// set (applyPendingEffort). Meanwhile the effort the agent reports is its
646+
// reset, never saved as a default. Dropped when the restored model lacks it
647+
// or the user picks a model or effort.
648+
struct PendingEffort
649+
{
650+
QString effort;
651+
QString model; // the model id the restore sent
652+
bool sent = false;
653+
};
654+
PendingEffort m_pendingEffort;
641655
// Restored session: the first prompt waits here while its agent resumes.
642656
struct ParkedSend
643657
{

‎tests/test_acp_protocol_serialization.cpp‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ private slots:
6666
void advisorModelFor_stepsUpOneLevel();
6767
void advisorModelFor_readsFamilyFromValueOrLabel();
6868
void advisorCommandText_onAndOff();
69+
void advisorIsToggle_readsTheHint();
70+
void advisorCommandText_followsTheAdvertisedForm();
6971
void parseAdvisorCommand_readsTypedToggle();
7072
void parseAgentCapabilities_specKeysAndLegacyFallback();
7173

@@ -603,6 +605,40 @@ void TestAcpProtocolSerialization::advisorCommandText_onAndOff()
603605
QStringLiteral("/advisor off"));
604606
}
605607

608+
namespace {
609+
// `/advisor` as each agent advertises it (captured 2026-11-02).
610+
AcpProtocol::AcpCommandInfo advisorCommand(const QString &hint)
611+
{
612+
return {QStringLiteral("advisor"), QStringLiteral("advisor"), hint};
613+
}
614+
const QString kClaudeAdvisorHint = QStringLiteral("[fable|opus|sonnet|off]"); // claude-agent-acp
615+
const QString kOmpAdvisorHint = QStringLiteral("[on|off|status|dump [raw]|configure]"); // omp 18.15
616+
} // namespace
617+
618+
void TestAcpProtocolSerialization::advisorIsToggle_readsTheHint()
619+
{
620+
QVERIFY(AcpProtocol::advisorIsToggle({advisorCommand(kOmpAdvisorHint)}));
621+
QVERIFY(!AcpProtocol::advisorIsToggle({advisorCommand(kClaudeAdvisorHint)}));
622+
QVERIFY(!AcpProtocol::advisorIsToggle({advisorCommand(QString())}));
623+
QVERIFY(!AcpProtocol::advisorIsToggle({}));
624+
// "on" must be an alternative of its own, not part of a word.
625+
QVERIFY(!AcpProtocol::advisorIsToggle({advisorCommand(QStringLiteral("[sonnet|off]"))}));
626+
}
627+
628+
void TestAcpProtocolSerialization::advisorCommandText_followsTheAdvertisedForm()
629+
{
630+
const QList<AcpProtocol::AcpCommandInfo> omp{advisorCommand(kOmpAdvisorHint)};
631+
const QList<AcpProtocol::AcpCommandInfo> claude{advisorCommand(kClaudeAdvisorHint)};
632+
QCOMPARE(AcpProtocol::advisorCommandText(true, omp, QStringLiteral("local-ccr/composer")),
633+
QStringLiteral("/advisor on"));
634+
QCOMPARE(AcpProtocol::advisorCommandText(false, omp, QStringLiteral("local-ccr/composer")),
635+
QStringLiteral("/advisor off"));
636+
QCOMPARE(AcpProtocol::advisorCommandText(true, claude, QStringLiteral("sonnet")),
637+
QStringLiteral("/advisor opus"));
638+
QCOMPARE(AcpProtocol::advisorCommandText(false, claude, QStringLiteral("sonnet")),
639+
QStringLiteral("/advisor off"));
640+
}
641+
606642
void TestAcpProtocolSerialization::parseAdvisorCommand_readsTypedToggle()
607643
{
608644
QCOMPARE(AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisor opus")),
@@ -614,7 +650,13 @@ void TestAcpProtocolSerialization::parseAdvisorCommand_readsTypedToggle()
614650
QCOMPARE(AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisor OFF")),
615651
std::optional<bool>(false));
616652
// Status query, lookalikes and ordinary prompts leave the toggle alone.
653+
QCOMPARE(AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisor on")),
654+
std::optional<bool>(true));
617655
QVERIFY(!AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisor")).has_value());
656+
// omp's queries and setup do not toggle it.
657+
QVERIFY(!AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisor status")).has_value());
658+
QVERIFY(!AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisor dump raw")).has_value());
659+
QVERIFY(!AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisor configure")).has_value());
618660
QVERIFY(!AcpProtocol::parseAdvisorCommand(QStringLiteral("/advisors opus")).has_value());
619661
QVERIFY(!AcpProtocol::parseAdvisorCommand(QStringLiteral("use /advisor off")).has_value());
620662
QVERIFY(!AcpProtocol::parseAdvisorCommand(QString()).has_value());

0 commit comments

Comments
 (0)