From 3935f825654a9527d8db1174a31a78f1f6443c81 Mon Sep 17 00:00:00 2001 From: shihasbacker Date: Tue, 27 Oct 2026 15:01:27 +0530 Subject: [PATCH 1/3] fix Android composer model visibility --- .../src/components/ComposerModelControl.tsx | 4 ++- .../src/features/threads/ThreadComposer.tsx | 36 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/mobile/src/components/ComposerModelControl.tsx b/apps/mobile/src/components/ComposerModelControl.tsx index e422cfabd4591..0f62a26b5e0bd 100644 --- a/apps/mobile/src/components/ComposerModelControl.tsx +++ b/apps/mobile/src/components/ComposerModelControl.tsx @@ -19,6 +19,8 @@ export function ComposerModelControl(props: { readonly disabled?: boolean; readonly size?: "regular" | "large"; readonly iconOnly?: boolean; + /** Keep the model name readable in a narrow row; the accessibility label still names the traits. */ + readonly hideTraits?: boolean; readonly onPress: () => void; }) { const descriptors = resolveProviderOptionDescriptors({ @@ -73,7 +75,7 @@ export function ComposerModelControl(props: { label={props.modelOption?.shortName ?? label} iconOnly={props.iconOnly} suffix={ - summary ? ( + summary && !props.hideTraits ? ( 0 && - (contentHeight > text.lineHeight * 1.5 || - (compactWidth > 0 && contentSize.width > compactWidth)); + forceExpanded || + (value.length > 0 && + (contentHeight > text.lineHeight * 1.5 || + (compactWidth > 0 && contentSize.width > compactWidth))); const compactRowHeight = Math.max(44, minimum); const editorStyle = useAnimatedStyle(() => ({ position: "absolute", @@ -266,6 +271,8 @@ export const COMPOSER_LAYOUT_TRANSITION = Platform.OS === "android" ? undefined : composerHeightTransition; // Share of the one-row composer the draft keeps; a long model name truncates first. const INLINE_EDITOR_MIN_WIDTH = "40%"; +// Android types in the expanded editor, so the unfocused row only previews the draft. +const ANDROID_PREVIEW_EDITOR_MIN_WIDTH = "25%"; const INLINE_EDITOR_HEIGHT = 36; const COMPOSER_ATTACHMENT_ENTERING = @@ -566,7 +573,12 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps useSidebarWorkspaceTabs() === "picker" || (Platform.OS === "ios" && Platform.isPad); const [compactRowWidth, setCompactRowWidth] = useState(0); const compactAttachmentLimit = compactRowWidth >= 500 ? 3 : 1; - const compactEditor = useCompactComposerEditor(bodyText, draftMessage); + // Android gives focused typing the full-width editor and a toolbar row for the model labels. + const compactEditor = useCompactComposerEditor( + bodyText, + draftMessage, + Platform.OS === "android" && isExpanded, + ); const showsCompactDictation = isVoiceInputPresented; const isToolbarVisible = isVoiceInputPresented; const hasEditingAttachments = @@ -776,7 +788,8 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps reportedSelection={props.reportedModelSelection} prompt={draftMessage} surfaceColor={composerPanel} - iconOnly={!compactEditor.isExpanded} + iconOnly={Platform.OS !== "android" && !compactEditor.isExpanded} + hideTraits={!compactEditor.isExpanded} onPress={openSettings} /> @@ -885,10 +898,13 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps onLayout={compactEditor.onEditorLayout} style={[ { - minWidth: - compactEditor.isExpanded || stripAttachments.length > 0 - ? 0 - : INLINE_EDITOR_MIN_WIDTH, + minWidth: compactEditor.isExpanded + ? 0 + : Platform.OS === "android" + ? ANDROID_PREVIEW_EDITOR_MIN_WIDTH + : stripAttachments.length > 0 + ? 0 + : INLINE_EDITOR_MIN_WIDTH, }, compactEditor.containerStyle, compactEditor.isExpanded ? { flex: 0, width: 0 } : undefined, From adaf098f62e9dd780c522d5326b34a9732cb6064 Mon Sep 17 00:00:00 2001 From: shihasbacker Date: Tue, 27 Oct 2026 15:33:29 +0530 Subject: [PATCH 2/3] add compact model and effort labels to Android composer --- .../src/components/ComposerModelControl.tsx | 52 ++++++++++++++--- .../mobile/src/components/ComposerToolbar.tsx | 4 +- .../src/features/threads/ThreadComposer.tsx | 14 ++++- .../client-runtime/src/modelTraits.test.ts | 57 +++++++++++++++++++ packages/client-runtime/src/modelTraits.ts | 30 +++++++++- 5 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 packages/client-runtime/src/modelTraits.test.ts diff --git a/apps/mobile/src/components/ComposerModelControl.tsx b/apps/mobile/src/components/ComposerModelControl.tsx index 0f62a26b5e0bd..d7641b39bc8b4 100644 --- a/apps/mobile/src/components/ComposerModelControl.tsx +++ b/apps/mobile/src/components/ComposerModelControl.tsx @@ -1,4 +1,6 @@ import type { ModelSelection, ProviderDriverKind } from "@t3tools/contracts"; +import { useState } from "react"; +import { View } from "react-native"; import { buildTraitsTriggerDisplay, getPromptControlledTraits, @@ -19,8 +21,10 @@ export function ComposerModelControl(props: { readonly disabled?: boolean; readonly size?: "regular" | "large"; readonly iconOnly?: boolean; - /** Keep the model name readable in a narrow row; the accessibility label still names the traits. */ - readonly hideTraits?: boolean; + /** Effort abbreviation instead of the full traits, for a row shared with the editor. */ + readonly compact?: boolean; + /** Width the control may fill; falls back to the effort abbreviation plus speed when the full traits overflow it. */ + readonly fitWidth?: number; readonly onPress: () => void; }) { const descriptors = resolveProviderOptionDescriptors({ @@ -28,7 +32,7 @@ export function ComposerModelControl(props: { selections: props.selection?.options, }); const promptTraits = getPromptControlledTraits(descriptors, props.prompt); - const summary = props.provider + const traits = props.provider ? buildTraitsTriggerDisplay({ provider: props.provider, descriptors, @@ -36,11 +40,20 @@ export function ComposerModelControl(props: { ultrathinkPromptControlled: promptTraits.ultrathinkPromptControlled, modelSelection: props.selection, reportedModelSelection: props.reportedSelection, - }).label - : ""; + }) + : null; + const summary = traits?.label ?? ""; + const [fullWidth, setFullWidth] = useState(0); + const overflows = props.fitWidth !== undefined && fullWidth > 0 && fullWidth > props.fitWidth + 1; + const compact = props.compact === true || overflows; const badge = props.modelOption?.providerBadge; const label = props.modelOption?.label ?? props.selection?.model ?? "Choose model"; - return ( + const visibleSummary = props.compact + ? (traits?.compactEffortLabel ?? "") + : overflows + ? [traits?.compactEffortLabel, traits?.speedLabel].filter(Boolean).join(" ") + : summary; + const renderControl = (full: boolean) => ( - {summary} + {full ? summary : visibleSummary} ) : undefined } - maxWidth="100%" + maxWidth={full ? 10_000 : "100%"} onPress={props.onPress} /> ); + if (props.fitWidth === undefined) return renderControl(false); + return ( + <> + {/* Unconstrained copy measures whether the full labels fit. */} + + setFullWidth(event.nativeEvent.layout.width)} + > + {renderControl(true)} + + + {renderControl(false)} + + ); } diff --git a/apps/mobile/src/components/ComposerToolbar.tsx b/apps/mobile/src/components/ComposerToolbar.tsx index f2b92eaf89260..c2374cb5a749f 100644 --- a/apps/mobile/src/components/ComposerToolbar.tsx +++ b/apps/mobile/src/components/ComposerToolbar.tsx @@ -33,6 +33,8 @@ export function ComposerInlineControl(props: { readonly renderIcon?: (size: number) => ReactNode; readonly label: string; readonly suffix?: ReactNode; + /** A short suffix keeps its width while the label truncates. */ + readonly suffixFixed?: boolean; /** Show only the icon and chevron; the label still names the control for accessibility. */ readonly iconOnly?: boolean; readonly maxWidth?: ViewStyle["maxWidth"]; @@ -99,7 +101,7 @@ export function ComposerInlineControl(props: { )} {!props.iconOnly && props.suffix ? ( - {props.suffix} + {props.suffix} ) : null} {props.showChevron === false ? null : ( = 500 ? 3 : 1; // Android gives focused typing the full-width editor and a toolbar row for the model labels. const compactEditor = useCompactComposerEditor( @@ -778,8 +779,16 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps ) : null; + const fitsModelToolbar = Platform.OS === "android" && compactEditor.isExpanded; const modelControl = ( - + setModelToolbarWidth(event.nativeEvent.layout.width) + : undefined + } + > 0 ? modelToolbarWidth : undefined} onPress={openSettings} /> diff --git a/packages/client-runtime/src/modelTraits.test.ts b/packages/client-runtime/src/modelTraits.test.ts new file mode 100644 index 0000000000000..5104583772ff6 --- /dev/null +++ b/packages/client-runtime/src/modelTraits.test.ts @@ -0,0 +1,57 @@ +import { ProviderDriverKind, type ProviderOptionDescriptor } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { buildTraitsTriggerDisplay } from "./modelTraits.ts"; + +function effort(currentValue: string): ProviderOptionDescriptor { + return { + id: "reasoningEffort", + label: "Reasoning", + type: "select", + currentValue, + options: [ + { id: "medium", label: "Medium" }, + { id: "high", label: "High" }, + { id: "xhigh", label: "Extra High" }, + { id: "custom", label: "Deep Think" }, + ], + }; +} + +function fastMode(currentValue: boolean): ProviderOptionDescriptor { + return { id: "fastMode", label: "Fast mode", type: "boolean", currentValue }; +} + +function display(descriptors: ReadonlyArray) { + return buildTraitsTriggerDisplay({ + provider: ProviderDriverKind.make("claudeAgent"), + descriptors, + primarySelectDescriptorId: "reasoningEffort", + ultrathinkPromptControlled: false, + }); +} + +describe("buildTraitsTriggerDisplay", () => { + it("abbreviates the selected effort", () => { + expect(display([effort("medium")]).compactEffortLabel).toBe("M"); + expect(display([effort("high")]).compactEffortLabel).toBe("H"); + expect(display([effort("xhigh")]).compactEffortLabel).toBe("XH"); + }); + + it("keeps an unknown effort label whole rather than inventing an abbreviation", () => { + expect(display([effort("custom")]).compactEffortLabel).toBe("Deep Think"); + }); + + it("reports Fast only while it is enabled and keeps the effort when it is off", () => { + expect(display([effort("high"), fastMode(true)])).toEqual({ + label: "High Fast", + compactEffortLabel: "H", + speedLabel: "Fast", + }); + expect(display([effort("high"), fastMode(false)])).toEqual({ + label: "High", + compactEffortLabel: "H", + speedLabel: null, + }); + }); +}); diff --git a/packages/client-runtime/src/modelTraits.ts b/packages/client-runtime/src/modelTraits.ts index 198d09e3a13c6..c505f37d78408 100644 --- a/packages/client-runtime/src/modelTraits.ts +++ b/packages/client-runtime/src/modelTraits.ts @@ -103,6 +103,25 @@ export function getTraitSelectChange( }; } +const COMPACT_EFFORT_LABELS: Record = { + none: "None", + minimal: "Min", + low: "L", + medium: "M", + high: "H", + "extra high": "XH", + xhigh: "XH", + max: "Max", +}; + +function compactEffortLabel(label: string): string { + const key = label + .toLowerCase() + .replace(/\s+effort$/, "") + .trim(); + return COMPACT_EFFORT_LABELS[key] ?? label; +} + export function buildTraitsTriggerDisplay(input: { provider: ProviderDriverKind; descriptors: ReadonlyArray; @@ -110,7 +129,7 @@ export function buildTraitsTriggerDisplay(input: { ultrathinkPromptControlled: boolean; modelSelection?: ModelSelection | null; reportedModelSelection?: ModelSelection | null | undefined; -}): { label: string } { +}): { label: string; compactEffortLabel: string | null; speedLabel: string | null } { let fastModeFallbackLabel: string | null = null; let speedLabel: string | null = null; let reasoningLabelIndex = -1; @@ -167,8 +186,13 @@ export function buildTraitsTriggerDisplay(input: { } } + const compact = { + compactEffortLabel: + reasoningLabelIndex >= 0 ? compactEffortLabel(labels[reasoningLabelIndex]!) : null, + speedLabel, + }; if (labels.length === 0 && fastModeFallbackLabel !== null) { - return { label: fastModeFallbackLabel }; + return { label: fastModeFallbackLabel, ...compact }; } if (speedLabel) { if (reasoningLabelIndex >= 0) { @@ -177,5 +201,5 @@ export function buildTraitsTriggerDisplay(input: { labels.push(speedLabel); } } - return { label: labels.join(" · ") }; + return { label: labels.join(" · "), ...compact }; } From 89341c89554fd42a23b463fb9e714c1e3b9beaf2 Mon Sep 17 00:00:00 2001 From: shihasbacker Date: Tue, 27 Oct 2026 15:59:53 +0530 Subject: [PATCH 3/3] let unrecognized effort names truncate in compact Android composer --- apps/mobile/src/components/ComposerModelControl.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/components/ComposerModelControl.tsx b/apps/mobile/src/components/ComposerModelControl.tsx index d7641b39bc8b4..41b7917667a0e 100644 --- a/apps/mobile/src/components/ComposerModelControl.tsx +++ b/apps/mobile/src/components/ComposerModelControl.tsx @@ -87,7 +87,8 @@ export function ComposerModelControl(props: { } label={props.modelOption?.shortName ?? label} iconOnly={props.iconOnly} - suffixFixed={!full && compact} + // Only short abbreviations keep their width; an unrecognized effort name truncates. + suffixFixed={!full && compact && (traits?.compactEffortLabel?.length ?? 0) <= 4} suffix={ (full ? summary : visibleSummary) ? (