mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 02:15:02 +00:00
Keep the composer's trailing action in place while typing (#9151)
The waveform and Send decided "is there content" separately: Send read the input's live text, while the waveform was gated on the draft store, whose web write lands after paint for typing performance. Between the two commits the toolbar showed both discs, moved the mic and context ring, or painted a disabled Send. MessageInput now picks voice mode, Send or stop from its live text. The composer only says whether voice mode is on offer. The focus hint reads the same live text, since the composer no longer rerenders when the draft gains or loses text.
This commit is contained in:
@@ -1150,21 +1150,14 @@ interface ComposerVoiceModeButtonProps {
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
function showsVoiceModeButton(input: {
|
||||
/** Whether voice mode can take the trailing disc; MessageInput decides when it does. */
|
||||
function offersVoiceMode(input: {
|
||||
isVoiceModeForAgent: boolean;
|
||||
hasAgent: boolean;
|
||||
isAgentRunning: boolean;
|
||||
hasSendableContent: boolean;
|
||||
showVoice: boolean;
|
||||
}): boolean {
|
||||
// Voice mode holds the trailing disc until there is something to send, then Send takes it.
|
||||
return (
|
||||
input.showVoice &&
|
||||
!input.isVoiceModeForAgent &&
|
||||
input.hasAgent &&
|
||||
!input.isAgentRunning &&
|
||||
!input.hasSendableContent
|
||||
);
|
||||
return input.showVoice && !input.isVoiceModeForAgent && input.hasAgent && !input.isAgentRunning;
|
||||
}
|
||||
|
||||
function ComposerVoiceModeButton({
|
||||
@@ -1326,11 +1319,6 @@ function ComposerContentImpl({
|
||||
const { finePointer: hasFinePointer } = usePointerCapabilities();
|
||||
const isDesktopLayout = resolveIsDesktopWebBreakpoint(isCompactLayout);
|
||||
const messagePlaceholder = resolveMessagePlaceholder(inputMode, isDesktopLayout, t, placeholder);
|
||||
const hasText = useSyncExternalStore(
|
||||
textSource.subscribe,
|
||||
() => textSource.getSnapshot().trim().length > 0,
|
||||
() => textSource.getSnapshot().trim().length > 0,
|
||||
);
|
||||
const setUserInput = onChangeText;
|
||||
const workspaceAttachments = useWorkspaceAttachmentsForScopes(attachmentScopeKeys);
|
||||
const {
|
||||
@@ -2042,8 +2030,6 @@ function ComposerContentImpl({
|
||||
],
|
||||
);
|
||||
|
||||
const hasSendableContent = hasText || selectedAttachments.length > 0;
|
||||
|
||||
// An open autocomplete owns the arrows; otherwise they may recall sent messages.
|
||||
const handleCommandKeyPress = useCallback(
|
||||
(event: ComposerKeyPressEvent) => {
|
||||
@@ -2098,11 +2084,10 @@ function ComposerContentImpl({
|
||||
|
||||
const voiceModeContent = useMemo(
|
||||
() =>
|
||||
showsVoiceModeButton({
|
||||
offersVoiceMode({
|
||||
isVoiceModeForAgent,
|
||||
hasAgent,
|
||||
isAgentRunning,
|
||||
hasSendableContent,
|
||||
showVoice: mode.showVoice,
|
||||
}) ? (
|
||||
<View style={styles.rightControls}>
|
||||
@@ -2121,7 +2106,6 @@ function ComposerContentImpl({
|
||||
toolbarGlyph,
|
||||
handleToggleRealtimeVoice,
|
||||
hasAgent,
|
||||
hasSendableContent,
|
||||
isAgentRunning,
|
||||
isConnected,
|
||||
isVoiceModeForAgent,
|
||||
|
||||
@@ -185,7 +185,7 @@ export interface MessageInputProps {
|
||||
leftContent?: React.ReactNode;
|
||||
/** Content to render on the right side before the voice button (e.g., context window meter) */
|
||||
beforeVoiceContent?: React.ReactNode;
|
||||
/** The realtime voice mode button after the dictation button, or null when the row hides it. */
|
||||
/** The realtime voice mode button, which takes the trailing disc while there is nothing to send. */
|
||||
voiceModeContent?: React.ReactNode;
|
||||
/** Primary action to render when the agent is active and the composer has no sendable content. */
|
||||
activeActionContent?: React.ReactNode;
|
||||
@@ -898,7 +898,7 @@ function SendButtonTooltip({
|
||||
);
|
||||
}
|
||||
|
||||
type PrimaryActionKind = "send" | "active" | "none";
|
||||
type PrimaryActionKind = "send" | "active" | "voiceMode" | "none";
|
||||
|
||||
function hasSendableComposerContent(input: {
|
||||
hasText: boolean;
|
||||
@@ -913,22 +913,26 @@ function resolvePrimaryActionKind(input: {
|
||||
allowEmptySubmit: boolean;
|
||||
isAgentRunning: boolean;
|
||||
isSubmitLoading: boolean;
|
||||
offersVoiceMode: boolean;
|
||||
}): PrimaryActionKind {
|
||||
if (input.hasSendableContent || input.allowEmptySubmit) return "send";
|
||||
if (input.isAgentRunning) return "active";
|
||||
if (input.isSubmitLoading) return "send";
|
||||
return "none";
|
||||
return input.offersVoiceMode ? "voiceMode" : "none";
|
||||
}
|
||||
|
||||
function PrimaryAction({
|
||||
kind,
|
||||
activeActionContent,
|
||||
voiceModeContent,
|
||||
...sendButtonProps
|
||||
}: {
|
||||
kind: PrimaryActionKind;
|
||||
activeActionContent: React.ReactNode;
|
||||
voiceModeContent: React.ReactNode;
|
||||
} & React.ComponentProps<typeof SendButtonTooltip>) {
|
||||
if (kind === "active") return activeActionContent;
|
||||
if (kind === "voiceMode") return voiceModeContent;
|
||||
if (kind === "send") return <SendButtonTooltip {...sendButtonProps} />;
|
||||
return null;
|
||||
}
|
||||
@@ -1886,6 +1890,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
allowEmptySubmit,
|
||||
isAgentRunning,
|
||||
isSubmitLoading,
|
||||
offersVoiceMode: voiceModeContent != null,
|
||||
});
|
||||
const { canPressLoadingButton, isSendButtonDisabled, defaultActionQueues } =
|
||||
computeSendButtonState({
|
||||
@@ -2023,7 +2028,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
// Fullscreen always shows Send; without content it is disabled, and looks it.
|
||||
// With nothing to send and no voice mode, a disabled Send holds the trailing disc so the row
|
||||
// does not shift when typing starts.
|
||||
const sendStandsIn = isFullscreen || (primaryActionKind === "none" && voiceModeContent == null);
|
||||
const sendStandsIn = isFullscreen || primaryActionKind === "none";
|
||||
const presentedActionKind: PrimaryActionKind = sendStandsIn ? "send" : primaryActionKind;
|
||||
const isPresentedSendDisabled = resolvePresentationSendDisabled({
|
||||
standsIn: sendStandsIn,
|
||||
@@ -2075,6 +2080,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
<PrimaryAction
|
||||
kind={presentedActionKind}
|
||||
activeActionContent={activeActionContent}
|
||||
voiceModeContent={voiceModeContent}
|
||||
shouldShow
|
||||
canPressLoadingButton={canPressLoadingButton}
|
||||
onSubmitLoadingPress={onSubmitLoadingPress}
|
||||
@@ -2096,6 +2102,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
[
|
||||
presentedActionKind,
|
||||
activeActionContent,
|
||||
voiceModeContent,
|
||||
canPressLoadingButton,
|
||||
onSubmitLoadingPress,
|
||||
handleDefaultSendAction,
|
||||
@@ -2146,7 +2153,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
fullscreen={isFullscreen}
|
||||
compact={isCompact}
|
||||
onRenderedLinesChange={setRenderedLines}
|
||||
focusHintVisible={!isFullscreen && isWeb && !isInputFocused && !value}
|
||||
focusHintVisible={!isFullscreen && isWeb && !isInputFocused && !hasLiveText}
|
||||
focusInputKeys={focusInputKeys}
|
||||
focusHintLabel={t("composer.input.focusHint", {
|
||||
shortcut: focusInputKeys ? formatShortcut(focusInputKeys[0], getShortcutOs()) : "",
|
||||
@@ -2178,6 +2185,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
isFullscreen,
|
||||
isCompact,
|
||||
isInputFocused,
|
||||
hasLiveText,
|
||||
value,
|
||||
focusInputKeys,
|
||||
],
|
||||
@@ -2215,7 +2223,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
{leftContent}
|
||||
</View>
|
||||
|
||||
{/* Right: voice button, contextual button (realtime/send/cancel) */}
|
||||
{/* Right: dictation, then the trailing action (voice mode, send or stop) */}
|
||||
<View style={styles.rightButtonGroup}>
|
||||
{beforeVoiceContent}
|
||||
<VoiceButtonTooltip
|
||||
@@ -2230,7 +2238,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
voiceMuteToggleKeys={voiceMuteToggleKeys}
|
||||
dictationToggleKeys={dictationToggleKeys}
|
||||
/>
|
||||
{voiceModeContent}
|
||||
{sendButton}
|
||||
</View>
|
||||
</View>
|
||||
@@ -2256,7 +2263,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
isRealtimeVoiceForCurrentAgent,
|
||||
voiceMuteToggleKeys,
|
||||
dictationToggleKeys,
|
||||
voiceModeContent,
|
||||
sendButton,
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user