From 6c68b1c60b842934db2ae23b3eaeccd9e314641c Mon Sep 17 00:00:00 2001 From: Kirill Markin Date: Wed, 2 Sep 2026 09:15:42 +0200 Subject: [PATCH] fix(mobile): drop the dead /model row and re-park the caret on draft switch #8587 landed the draft composer trigger menus. Two details it left out. `/model` bypassed the built-in capability gate, so it rendered on the new-task draft screen, where selecting it only inserts the literal text `/model `: `onSelect` has no `model` branch, and the `slash-model` trigger that follows builds no items. It is now gated on `hasThread`, so the thread composer is unchanged and the draft screen, which already carries its own model control, loses the dead row. The `ownerKey` caret reset ran in an effect, one commit too late, so switching drafts could flash the trigger popover open over a draft the user never typed a trigger into. It now adjusts during render, with the ref assigned before `setSelection` so the restarted render cannot loop. Model: Claude Opus 5, harness: Claude Code. Co-Authored-By: Claude Opus 5 --- .../features/threads/use-composer-command-menu.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.ts b/apps/mobile/src/features/threads/use-composer-command-menu.ts index 966ceedeec85..f8553b44d7b5 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.ts @@ -63,11 +63,13 @@ export function useComposerCommandMenu({ return { start, end: selectionEnd }; }); }, [draftMessage.length]); - useEffect(() => { - if (previousOwnerKeyRef.current === ownerKey) return; + // Adjusted while rendering, not in an effect: one committed render with the + // previous draft's caret is enough to flash the popover open over a draft + // the user never typed a trigger into. + if (previousOwnerKeyRef.current !== ownerKey) { previousOwnerKeyRef.current = ownerKey; setSelection(composerSelectionAtEnd(draftMessage)); - }, [draftMessage, ownerKey]); + } const trigger = useMemo(() => { if (!enabled || selection.start !== selection.end) { @@ -109,10 +111,14 @@ export function useComposerCommandMenu({ description: "Switch to default mode", }, ]; + // On an unsent draft `/model` is a dead row: `onSelect` has no `model` + // branch, so it inserts the literal text `/model `, and the `slash-model` + // trigger that follows builds no items, closing the menu. The draft screen + // has its own model control, so gate the row on an existing thread. const builtIn = allBuiltIn.filter( (item) => item.command.includes(q) && - (item.command === "model" || onUpdateInteractionMode !== undefined), + (item.command === "model" ? hasThread : onUpdateInteractionMode !== undefined), ); // A provider expands a slash command only when it opens the whole