-
Notifications
You must be signed in to change notification settings - Fork 5.1k
feat(swift-ios): choose the reasoning level from the composer #7344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
saphid
wants to merge
9
commits into
pingdotgg:t3code/rebuild-mobile-app-swift
Choose a base branch
from
saphid:feat/issue110-reasoning-selector
base: t3code/rebuild-mobile-app-swift
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2fdb36f
feat(swift-ios): show reasoning level in composer
saphid a8b3707
feat(swift-ios): choose reasoning level from the composer
saphid 6f3ae43
feat(swift-ios): drop the ultra tiers and pin composer reasoning order
saphid b849a88
fix(swift-ios): enlarge reasoning selector tap target
saphid 1fca1a9
fix(swift-ios): materialize composer reasoning default
saphid 696f9d4
Merge remote-tracking branch 'refs/remotes/upstream/t3code/rebuild-mo…
saphid d8de11f
feat(swift-ios): match composer traits picker
saphid 46b2288
fix(swift-ios): preserve hidden trait selections
saphid 2524de5
fix(swift-ios): show hidden effective traits
saphid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
239 changes: 239 additions & 0 deletions
239
apps/swift-ios/Features/Chat/FeatureComposerTraitsControl.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| /// The composer traits menu is derived from the selected model's option | ||
| /// descriptors. It deliberately knows nothing about which providers expose | ||
| /// which sections: select descriptors use their advertised choices, boolean | ||
| /// descriptors use On/Off, and descriptors without a usable control disappear. | ||
| struct FeatureComposerTraitsControl: Equatable { | ||
| struct Choice: Identifiable, Equatable { | ||
| let id: String | ||
| let label: String | ||
| let detail: String? | ||
| let isDefault: Bool | ||
| let value: FeatureModelOptionValue | ||
| } | ||
|
|
||
| struct Section: Identifiable, Equatable { | ||
| let id: String | ||
| let label: String | ||
| let choices: [Choice] | ||
| let currentChoiceID: String | ||
| } | ||
|
|
||
| let sections: [Section] | ||
| let triggerLabel: String | ||
| let showsFastModeIcon: Bool | ||
| private let resolvedSelection: FeatureSelection | ||
|
|
||
| static func resolve( | ||
| explicit: FeatureSelection?, | ||
| inherited: FeatureSelection?, | ||
| providers: [FeatureProvider], | ||
| materializesDefaultSelection: Bool | ||
| ) -> FeatureComposerTraitsControl? { | ||
| let providers = ProviderModelCatalogNormalizer.normalized(providers) | ||
| let selection = if materializesDefaultSelection { | ||
| ProviderModelSelectionResolver.materialized(explicit, in: providers) | ||
| } else { | ||
| ThreadComposerModelSelectionPolicy.resolvedSelection( | ||
| explicit: explicit, | ||
| inherited: inherited, | ||
| providers: providers | ||
| ) | ||
| } | ||
| guard let selection, | ||
| let provider = providers.first(where: { $0.id == selection.providerID }), | ||
| let model = provider.models.first(where: { $0.id == selection.modelID }) else { | ||
| return nil | ||
| } | ||
|
|
||
| let sections = model.options.compactMap { | ||
| section(for: $0, selections: selection.options) | ||
| } | ||
| guard !sections.isEmpty else { return nil } | ||
|
|
||
| let trigger = triggerDisplay( | ||
| sections: sections, | ||
| descriptors: model.options, | ||
| providerDriver: provider.driver | ||
| ) | ||
| return FeatureComposerTraitsControl( | ||
| sections: sections, | ||
| triggerLabel: trigger.label, | ||
| showsFastModeIcon: trigger.showsFastModeIcon, | ||
| resolvedSelection: selection | ||
| ) | ||
| } | ||
|
|
||
| /// A traits choice writes through the same selection binding as the model | ||
| /// picker. The effective values of every visible descriptor are materialized | ||
| /// at the same time, matching Electron and preventing neighboring defaults | ||
| /// from disappearing on the next turn. | ||
| func selection(choosing choiceID: String, in descriptorID: String) -> FeatureSelection { | ||
| guard let section = sections.first(where: { $0.id == descriptorID }), | ||
| let choice = section.choices.first(where: { $0.id == choiceID }) else { | ||
| return resolvedSelection | ||
| } | ||
|
|
||
| var next = resolvedSelection | ||
| for section in sections { | ||
| guard let current = section.choices.first(where: { | ||
| $0.id == section.currentChoiceID | ||
| }) else { continue } | ||
| next.options = DailyUXModelOptions.updating( | ||
| next.options, | ||
| id: section.id, | ||
| value: current.value | ||
| ) | ||
| } | ||
| next.options = DailyUXModelOptions.updating( | ||
| next.options, | ||
| id: descriptorID, | ||
| value: choice.value | ||
| ) | ||
| return next | ||
| } | ||
|
|
||
| private static func section( | ||
| for descriptor: FeatureModelOptionDescriptor, | ||
| selections: [FeatureModelOptionSelection] | ||
| ) -> Section? { | ||
| switch descriptor.kind { | ||
| case .select: | ||
| let supportedChoices = descriptor.choices.filter { | ||
| !(descriptor.promptInjectedValues ?? []).contains($0.id) | ||
| } | ||
| guard !supportedChoices.isEmpty else { return nil } | ||
| return Section( | ||
| id: descriptor.id, | ||
| label: descriptor.label, | ||
| choices: supportedChoices.map { | ||
| Choice( | ||
| id: $0.id, | ||
| label: $0.label, | ||
| detail: $0.detail, | ||
| isDefault: $0.isDefault, | ||
| value: .string($0.id) | ||
| ) | ||
| }, | ||
| currentChoiceID: currentSelectChoiceID( | ||
| for: descriptor, | ||
| among: supportedChoices, | ||
| selections: selections | ||
| ) | ||
| ) | ||
| case .boolean: | ||
| let current = currentBooleanValue(for: descriptor, selections: selections) | ||
| return Section( | ||
| id: descriptor.id, | ||
| label: descriptor.label, | ||
| choices: [ | ||
| Choice( | ||
| id: "on", | ||
| label: "On", | ||
| detail: nil, | ||
| isDefault: false, | ||
| value: .boolean(true) | ||
| ), | ||
| Choice( | ||
| id: "off", | ||
| label: "Off", | ||
| detail: nil, | ||
| isDefault: false, | ||
| value: .boolean(false) | ||
| ), | ||
| ], | ||
| currentChoiceID: current ? "on" : "off" | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private static func currentSelectChoiceID( | ||
| for descriptor: FeatureModelOptionDescriptor, | ||
| among choices: [FeatureModelOptionChoice], | ||
| selections: [FeatureModelOptionSelection] | ||
| ) -> String { | ||
| if case .string(let selected)? = selections.first(where: { | ||
| $0.id == descriptor.id | ||
| })?.value, | ||
| choices.contains(where: { $0.id == selected }) | ||
| || (descriptor.promptInjectedValues ?? []).contains(selected) { | ||
| return selected | ||
|
macroscopeapp[bot] marked this conversation as resolved.
|
||
| } | ||
| if case .string(let defaultID) = descriptor.defaultValue, | ||
| choices.contains(where: { $0.id == defaultID }) { | ||
| return defaultID | ||
| } | ||
| return choices.first(where: \.isDefault)?.id ?? choices[0].id | ||
|
saphid marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private static func currentBooleanValue( | ||
| for descriptor: FeatureModelOptionDescriptor, | ||
| selections: [FeatureModelOptionSelection] | ||
| ) -> Bool { | ||
| if case .boolean(let selected)? = selections.first(where: { | ||
| $0.id == descriptor.id | ||
| })?.value { | ||
| return selected | ||
| } | ||
| if case .boolean(let defaultValue) = descriptor.defaultValue { | ||
| return defaultValue | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| /// Mirrors Electron's compact TraitsPicker display. Fast mode is a bolt when | ||
| /// another trait supplies readable text; when it is the only trait its state | ||
| /// remains text so the trigger never becomes an unexplained icon. | ||
| private static func triggerDisplay( | ||
| sections: [Section], | ||
| descriptors: [FeatureModelOptionDescriptor], | ||
| providerDriver: String | ||
| ) -> (label: String, showsFastModeIcon: Bool) { | ||
| var fastModeFallbackLabel: String? | ||
| var fastModeEnabled = false | ||
| var labels: [String] = [] | ||
|
|
||
| for descriptor in descriptors { | ||
| guard let section = sections.first(where: { $0.id == descriptor.id }) else { | ||
| continue | ||
| } | ||
| let current = section.choices.first(where: { | ||
| $0.id == section.currentChoiceID | ||
| }) | ||
|
|
||
| if descriptor.id == "fastMode", descriptor.kind == .boolean { | ||
| fastModeEnabled = current?.value == .boolean(true) | ||
| fastModeFallbackLabel = fastModeEnabled ? "Fast" : "Normal" | ||
| continue | ||
| } | ||
|
|
||
| if providerDriver == "codex", | ||
| descriptor.id == "serviceTier", | ||
| descriptor.kind == .select, | ||
| let fastChoice = section.choices.first(where: { $0.label == "Fast" }), | ||
| section.currentChoiceID == "default" | ||
| || section.currentChoiceID == fastChoice.id { | ||
| fastModeEnabled = section.currentChoiceID == fastChoice.id | ||
| fastModeFallbackLabel = current?.label | ||
| continue | ||
| } | ||
|
|
||
| switch descriptor.kind { | ||
| case .select: | ||
| let label = current?.label ?? descriptor.choices.first(where: { | ||
| $0.id == section.currentChoiceID | ||
| })?.label | ||
| if let label { | ||
| labels.append(label) | ||
| } | ||
| case .boolean: | ||
| guard case .boolean(let value)? = current?.value else { continue } | ||
| labels.append("\(descriptor.label) \(value ? "On" : "Off")") | ||
| } | ||
| } | ||
|
|
||
| if labels.isEmpty, let fastModeFallbackLabel { | ||
| return (fastModeFallbackLabel, false) | ||
| } | ||
| return (labels.joined(separator: " · "), fastModeEnabled) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.