Skip to content

feat(ios): hide the thinking-level control for models without reasoning support#100875

Merged
steipete merged 1 commit into
mainfrom
feat/ios-thinking-gating-nash
Jul 6, 2026
Merged

feat(ios): hide the thinking-level control for models without reasoning support#100875
steipete merged 1 commit into
mainfrom
feat/ios-thinking-gating-nash

Conversation

@steipete

@steipete steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Related: #100699

What Problem This Solves

Fixes an issue where iOS/macOS chat users would see and set a thinking-level control even when the selected model does not support configurable reasoning effort — the control silently did nothing for those models, which reads as broken.

Why This Change Was Made

The gateway already computes per-model thinking capability: models with catalog reasoning: false resolve to an off-only thinking profile, and models.list carries a per-model reasoning flag. This PR consumes that existing metadata client-side — no protocol changes. The shared chat view model now hides the thinking picker when (a) gateway-provided session/default thinking metadata resolves to off-only, or (b) the locally selected model choice carries reasoning == false (which covers the window right after a model switch, before the session metadata refreshes). Legacy gateways that send no thinking metadata keep the picker (fail open). While the control is gated off, sends carry an effective thinking level of off instead of the stored preference — the gateway treats an explicit unsupported level as a hard error (agent-command.ts throws for explicit levels the model doesn't support), and with the control hidden the user would have no way to fix a stale non-off level. The stored preference itself is never mutated, so switching back to a reasoning-capable model restores the user's level. Session thinking metadata is preserved across same-model patches but deliberately dropped when the model changes (stale metadata must not gate the new model). The reasoning flag is threaded through OpenClawChatModelChoice and both transports (iOS gateway transport decode, macOS webchat mapping) with default-nil compatibility for existing call sites.

User Impact

The thinking-level control now appears only when it actually does something for the selected model. Switching to a non-reasoning model hides it immediately; switching back restores it.

Evidence

  • swift test --package-path apps/shared/OpenClawKit — full suite green; ChatViewModelTests now 147 tests including: off-only metadata hides, multi-level shows, metadata-absent legacy shows, local reasoning == false hides immediately despite stale multi-level metadata, default-sentinel selection resolves the session model for gating, the current-level augmentation cannot flip gating, same-model patches preserve thinking metadata while model changes drop it, and gated sends carry off while the stored level survives; ChatViewModelOutboxTests (20) cover queued/retry sends re-evaluating the gate per session at flush time.
  • iOS xcodebuild build-for-testing + focused IOSGatewayChatTransportTests green (reasoning decode assertion added); macOS swift build green.
  • swiftformat --lint via the iOS filelist — clean.
  • Structured review (codex) — clean at head.

@openclaw-barnacle openclaw-barnacle Bot added app: ios App: ios app: macos App: macos size: L maintainer Maintainer-authored PR labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de0cd20782

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +21 to +23
showsPicker = self.thinkingPickerIsAvailable(
for: session,
modelChoice: self.sessionModelChoice(for: session))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve queued thinking when session metadata is absent

When flushing a queued command for a background/older session that is not present in the currently loaded self.sessions list, session is nil here, but sessionModelChoice(for: nil) and resolvedThinkingLevelOptions(for: nil) fall back to the current defaults; if those defaults are off-only or reasoning == false, the queued command is downgraded to "off". I checked performOutboxFlush loads all persisted commands while bootstrap only refreshes a limited sessions list, so this is reachable and regresses the previous behavior of sending the level captured on the command.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the feat/ios-thinking-gating-nash branch from de0cd20 to 1b53863 Compare July 6, 2026 12:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b5386363c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let resolved = self.resolvedThinkingLevelOptions(for: session)
let gatewayAllowsOnlyOff = resolved.isGatewayMetadata &&
resolved.options.allSatisfy { $0.id == "off" }
return !gatewayAllowsOnlyOff && modelChoice?.reasoning != false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect gateway thinking metadata before hiding picker

For models whose provider thinking profile is explicitly preserved even when the catalog reports reasoning: false (for example the preserveWhenCatalogReasoningFalse paths in src/auto-reply/thinking.ts and Claude Fable/Gemini policies), sessions.list can return multiple thinkingLevels while models.list still exposes reasoning: false. This check hides the picker and makes sends use off anyway, so those supported model-specific thinking levels become unavailable in the native chat UI. Prefer treating non-off gateway thinkingLevels as authoritative, or expose the same preservation contract through the model choice.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 6, 2026, 10:30 AM ET / 14:30 UTC.

Summary
The PR adds optional model reasoning metadata to native chat model choices and uses it with gateway thinking metadata to hide the iOS/macOS thinking picker and gate send/outbox thinking levels.

PR surface: Other +610. Total +610 across 11 files.

Reproducibility: yes. Source inspection shows non-off gateway thinkingLevels plus model reasoning == false reaches the PR head gate and hides thinking, and the outbox/bootstrap send paths can be traced without running the app.

Review metrics: 2 noteworthy metrics.

  • Native model-choice capability field: 1 optional field added. The shared Swift model choice now carries reasoning, so merge review must confirm nil compatibility and precedence against gateway session metadata.
  • Queued-send thinking handling: 1 persisted command field behavior changed. The outbox capture path now stores a gated value instead of the raw preference, which changes session-state behavior for queued sends.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Fix metadata precedence so non-off gateway thinking metadata keeps the picker and stored send level, with a regression test for catalog reasoning == false plus preserved non-off metadata.
  • Preserve raw queued thinking at enqueue and keep sends gated until session/model metadata is initialized or authoritative absence is known.
  • [P1] Add redacted real iOS/macOS app proof showing the picker hides for unsupported models and reappears for supported models.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body lists tests/builds but no redacted real iOS/macOS screenshot, recording, live output, or logs showing the picker hide/show behavior after the change. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] Merging as-is can hide supported native thinking levels for provider profiles where gateway session metadata exposes non-off thinkingLevels while generic model catalog reasoning is false.
  • [P1] Merging as-is can silently downgrade queued sends to off when session metadata is missing or when an offline queued send is captured before model state settles.
  • [P1] Merging as-is can still send a restored non-off thinking level during bootstrap before sessions/models populate the new gate, which the gateway rejects for off-only models.
  • [P1] The PR body lists tests and builds but no redacted real iOS/macOS chat proof showing the picker hiding and reappearing in a real app run.

Maintainer options:

  1. Repair metadata precedence and send gating (recommended)
    Update native gating so non-off gateway metadata wins, absent queued-session metadata preserves the stored level, offline enqueue stores raw thinking, and bootstrap cannot send before the gate is initialized.
  2. Accept provider UX regression intentionally
    Maintainers may merge the catalog-first behavior only if they intentionally want native clients to disable provider-preserved thinking profiles despite gateway support.
  3. Pause for real app proof
    Leave the PR open but blocked until redacted iOS/macOS proof shows hide/show behavior and the chosen send-gating behavior in a real chat run.

Next step before merge

  • [P1] Protected maintainer label, unresolved provider-metadata direction, blocking session-state findings, and missing contributor real-app proof make this human-handled rather than a safe repair-queue item.

Maintainer decision needed

  • Question: Should native chat honor gateway-resolved non-off thinkingLevels over model catalog reasoning == false, or intentionally hide thinking whenever the selected model choice says reasoning == false?
  • Rationale: Current gateway code has an explicit provider-authoritative exception for stale catalog reasoning metadata, while the PR head creates a native catalog-first behavior that can hide supported provider levels.
  • Likely owner: steipete — steipete owns the linked native chat polish direction and recently merged the adjacent shared session-control work.
  • Options:
    • Honor gateway metadata (recommended): Treat non-off session/default thinkingLevels as authoritative, then use model reasoning == false only when gateway metadata is absent or off-only.
    • Accept catalog-first native gating: Keep the current PR direction only if maintainers intentionally want native chat to ignore provider-authoritative thinking profiles and own the compatibility regression.
    • Pause until proof and policy align: Keep the PR open but unmergeable until the contributor updates the code path and posts real app proof for the chosen behavior.

Security
Cleared: The diff is limited to native chat UI/model metadata handling, generated i18n line updates, and Swift tests; I found no concrete security or supply-chain concern.

Review findings

  • [P2] Honor gateway thinking metadata before model reasoning — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Thinking.swift:50
  • [P2] Preserve queued thinking when metadata is absent — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Thinking.swift:21-23
  • [P2] Store raw thinking for offline commands — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Outbox.swift:72
Review details

Best possible solution:

Make gateway session/default thinking metadata authoritative, preserve raw queued thinking until flush-time gating, initialize or block the send gate until model/session metadata is loaded, and require redacted real iOS/macOS proof before merge.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows non-off gateway thinkingLevels plus model reasoning == false reaches the PR head gate and hides thinking, and the outbox/bootstrap send paths can be traced without running the app.

Is this the best way to solve the issue?

No. The native surface is plausible, but the current implementation is not the best fix because it lets generic model metadata override gateway-resolved thinking support and gates queued/bootstrap sends before all model state is known.

Full review comments:

  • [P2] Honor gateway thinking metadata before model reasoning — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Thinking.swift:50
    When the gateway returns non-off thinkingLevels, it has already resolved provider-specific support, including the current preserveWhenCatalogReasoningFalse path. This reasoning == false check still hides the picker and forces sends to off, so supported provider levels become unavailable in native chat.
    Confidence: 0.91
  • [P2] Preserve queued thinking when metadata is absent — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Thinking.swift:21-23
    For a queued command whose background session is not in self.sessions, session is nil but this still falls through to session defaults and can downgrade the queued value based on the current/default model. Missing metadata should fail open and preserve next.thinking.
    Confidence: 0.88
  • [P2] Store raw thinking for offline commands — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Outbox.swift:72
    Offline enqueue can run while a model patch is still in flight, so storing effectiveThinkingLevelForSend can persist off before the final model state is known. Capture the raw stored preference here and let the flush-time gate decide after pending model patches settle.
    Confidence: 0.89
  • [P2] Gate sends until thinking metadata is initialized — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift:22
    showsThinkingPicker starts true, and bootstrap marks health healthy before fetchSessions/fetchModels populate the new gate; canSend does not block that loading window. A restored non-off preference can therefore be sent for an off-only model and hit the gateway unsupported-thinking error. This was visible at an earlier reviewed head, so this is a late discovery.
    Confidence: 0.84
    Late finding: first raised on code an earlier review cycle already covered.

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 94a0d2ec3b79.

Label changes

Label justifications:

  • P2: This is a normal-priority native chat UX improvement with concrete provider-metadata and queued-send regression risks, not an emergency or blocked setup path.
  • merge-risk: 🚨 auth-provider: The diff changes model capability handling and can disable supported provider-specific thinking behavior for existing native chat users.
  • merge-risk: 🚨 session-state: The diff changes how queued session commands preserve the user's thinking preference and can persist or send downgraded state before model/session metadata settles.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body lists tests/builds but no redacted real iOS/macOS screenshot, recording, live output, or logs showing the picker hide/show behavior after the change. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Other +610. Total +610 across 11 files.

View PR surface stats
Area Files Added Removed Net
Source 0 0 0 0
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 11 670 60 +610
Total 11 670 60 +610

What I checked:

Likely related people:

  • steipete: Recent merged work on shared native chat/session controls touched ChatViewModel.swift and IOSGatewayChatTransport.swift, and the linked polish-pack issue is in the same native chat UX area. (role: recent area contributor and likely product routing owner; confidence: high; commits: 88f1ec38d4a5, 6f7162101ada; files: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift, apps/ios/Sources/Chat/IOSGatewayChatTransport.swift, apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift)
  • ImLukeF: Commit 061b825 added the macOS chat model selector and thinking persistence across the shared chat view model and transport surfaces this PR extends. (role: original adjacent feature author; confidence: medium; commits: 061b8258bc35; files: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift, apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift, apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift)
  • vincentkoc: Current-main blame/log for the extracted shared thinking and outbox helper files points at cbf9acf, though that commit is broad and not specific to this PR's behavior. (role: recent source-history owner; confidence: low; commits: cbf9acff374c; files: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Thinking.swift, apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Outbox.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (2 earlier review cycles)
  • reviewed 2026-07-06T12:33:28.547Z sha 1b53863 :: needs real behavior proof before merge. :: [P2] Respect gateway thinking metadata before hiding picker
  • reviewed 2026-07-06T13:43:18.523Z sha 6e355a0 :: needs real behavior proof before merge. :: [P2] Let gateway thinking metadata win before hiding | [P2] Fail open for queued sessions with missing metadata | [P2] Store raw thinking before offline gating

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. labels Jul 6, 2026
@steipete
steipete force-pushed the feat/ios-thinking-gating-nash branch from 1b53863 to 6e355a0 Compare July 6, 2026 13:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e355a00cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

sessionKey: session.key,
text: text,
thinking: self.thinkingLevel,
thinking: self.effectiveThinkingLevelForSend,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture the queued thinking preference before gating

When a text send is queued while a model change that hides the picker is still in flight, this stores off permanently. If that sessions.patch(model) later fails or rolls back to a reasoning-capable model, performOutboxFlush correctly waits for the patch and re-evaluates the gate, but it can only re-evaluate the already-downgraded queued value, so the command sends without the user's stored reasoning level. Capture the raw thinkingLevel here and let the flush-time gate decide, matching the live send path that gates after waiting for model patches.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added the merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. label Jul 6, 2026
@steipete
steipete force-pushed the feat/ios-thinking-gating-nash branch from 6e355a0 to 6f71621 Compare July 6, 2026 14:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f7162101a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// Setter is module-internal for the thinking-level extension only.
public internal(set) var thinkingLevelOptions: [OpenClawChatThinkingLevelOption]
/// Setter is module-internal for the thinking-level extension only.
public internal(set) var showsThinkingPicker = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate thinking until bootstrap metadata arrives

When macOS restores a persisted non-off thinking level, a fresh bootstrap can make healthOK true after requestHistory/pollHealthIfNeeded but before fetchSessions/fetchModels populate this new gate, while canSend still allows a draft during loading. Because showsThinkingPicker starts true and applyHistoryPayload does not project payload.sessionInfo thinking/model metadata into sessions, effectiveThinkingLevelForSend can forward high/medium for a non-reasoning or off-only model in that window; src/agents/agent-command.ts rejects explicit unsupported thinking levels. Initialize the gate from available history/session metadata or keep sends/outbox capture gated until the session/model metadata sync completes.

Useful? React with 👍 / 👎.

@steipete
steipete merged commit 538d4ee into main Jul 6, 2026
77 checks passed
@steipete
steipete deleted the feat/ios-thinking-gating-nash branch July 6, 2026 14:59
@steipete

steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Merged via squash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: ios App: ios app: macos App: macos maintainer Maintainer-authored PR merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: L status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant