Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
useNavigation,
usePreventRemove,
} from "@react-navigation/native";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Alert, Platform, Pressable, ScrollView, View } from "react-native";
import {
KeyboardController,
Expand Down Expand Up @@ -34,7 +34,10 @@ import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStri
import { ProviderIcon } from "../../components/ProviderIcon";
import { SymbolView } from "../../components/AppSymbol";
import { AppText as Text } from "../../components/AppText";
import { ComposerCommandPopover } from "./ComposerCommandPopover";
import { ComposerSurface } from "./ThreadComposer";
import type { ComposerBuiltInCommand } from "./composer-trigger-menu";
import { useComposerTriggerMenu } from "./use-composer-trigger-menu";
import {
useThreadSettingsSheetPresentation,
type NavigationWithFinishTransitioning,
Expand Down Expand Up @@ -67,6 +70,12 @@ import {
} from "./new-task-context-presentation";
import { useIncomingShare } from "../sharing/IncomingShareProvider";

// `/model` is left out because it only inserts dead text on mobile: nothing
// handles the `slash-model` trigger the inserted text becomes. Plan and
// default only appear when the flow actually honors an interaction mode.
const DRAFT_BUILT_IN_COMMANDS: ReadonlyArray<ComposerBuiltInCommand> = ["plan", "default"];
const NO_BUILT_IN_COMMANDS: ReadonlyArray<ComposerBuiltInCommand> = [];

function NewTaskWorkspaceIcon(props: {
readonly workspaceMode: "local" | "worktree";
readonly worktreePath: string | null;
Expand Down Expand Up @@ -636,6 +645,34 @@ export function NewTaskDraftScreen(props: {
[flow],
);

const selectedProviderStatus = useMemo(
() =>
selectedEnvironmentServerConfig?.providers.find(
(provider) => provider.instanceId === flow.selectedModel?.instanceId,
) ?? null,
[selectedEnvironmentServerConfig, flow.selectedModel?.instanceId],
);
// Mentions resolve against the directory this task will actually run in:
// a new worktree does not exist yet, so those drafts search the checkout.
// Empty paths fall through to null — a queued task edited without a cwd
// snapshot carries `workspaceRoot: ""`, which is not a directory to search.
const composerSearchCwd =
flow.workspaceMode === "worktree"
? selectedProject?.workspaceRoot || null
: flow.selectedWorktreePath || selectedProject?.workspaceRoot || null;
const composerTriggerMenu = useComposerTriggerMenu({
text: flow.prompt,
environmentId: selectedProject?.environmentId ?? null,
projectCwd: composerSearchCwd,
provider: selectedProviderStatus,
builtInCommands: flow.planModeEnabled ? DRAFT_BUILT_IN_COMMANDS : NO_BUILT_IN_COMMANDS,
// The screen stays mounted across draft switches, so the caret has to be
// re-parked when the composer starts pointing at another project's draft.
resetKey: flow.draftKey,
onChangeText: flow.setPrompt,
onSelectInteractionMode: flow.setInteractionMode,
});

async function handleStart(): Promise<void> {
const selectedProject = flow.selectedProject;
const draftKey = flow.draftKey;
Expand Down Expand Up @@ -806,6 +843,11 @@ export function NewTaskDraftScreen(props: {
!isImportingShare &&
!flow.submitting &&
!(flow.workspaceMode === "worktree" && !flow.selectedBranchName);
// A share transfer locks the editor, so its rows must not be tappable.
const showComposerTriggerMenu =

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.

🟡 Medium threads/NewTaskDraftScreen.tsx:847

When an @ search is loading or returns no matches, the trigger menu disappears without showing ComposerCommandPopover, so users receive no loading or empty-results feedback. The items.length > 0 guard makes the popover's isLoading and empty-state rendering unreachable; render it whenever a trigger is active.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/NewTaskDraftScreen.tsx around line 847:

When an `@` search is loading or returns no matches, the trigger menu disappears without showing `ComposerCommandPopover`, so users receive no loading or empty-results feedback. The `items.length > 0` guard makes the popover's `isLoading` and empty-state rendering unreachable; render it whenever a trigger is active.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Checked this against origin/main: the gate is pre-existing, not introduced here.

ThreadComposer.tsx:725 on main already reads composerTrigger && composerMenuItems.length > 0, so ComposerCommandPopover's empty-state branch — and the emptyText / "Searching files…" copy at ComposerCommandPopover.tsx:89-91,177 — was already unreachable on mobile before this PR. This patch keeps the same gate on both call sites so the two surfaces behave identically, which is what the "thread composer keeps its previous behavior exactly" constraint requires.

Relaxing it is a real improvement, but it is a deliberate UX change to both surfaces (a popover that appears on a bare @ and shows a spinner), so it belongs in its own PR rather than riding along here. Happy to send that follow-up if you want it — the one-line change is the items.length > 0 half of the condition at both call sites.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

!isIncomingShareTransferPending &&
composerTriggerMenu.trigger !== null &&
composerTriggerMenu.items.length > 0;
const promptEditor = (
<ComposerEditor
ref={promptInputRef}
Expand All @@ -817,7 +859,9 @@ export function NewTaskDraftScreen(props: {
scrollEnabled
value={flow.prompt}
skills={flow.selectedProviderSkills}
selection={composerTriggerMenu.selection}
onChangeText={flow.setPrompt}
onSelectionChange={composerTriggerMenu.onSelectionChange}
onFocus={() => setIsComposerFocused(true)}
onBlur={() => setIsComposerFocused(false)}
onPasteImages={(uris) => void handleNativePasteImages(uris)}
Expand Down Expand Up @@ -956,6 +1000,20 @@ export function NewTaskDraftScreen(props: {

const composerDock = (
<View className="bg-sheet px-4 pt-1" style={{ paddingBottom: controlsBottomPadding }}>
{/* Zero-height anchor so the popover floats above the dock without
shifting the controls below it, as it does in the thread composer. */}
{showComposerTriggerMenu ? (
<View>
<View className="absolute inset-x-0 bottom-full z-10 mb-2">
<ComposerCommandPopover
items={composerTriggerMenu.items}
triggerKind={composerTriggerMenu.trigger?.kind ?? null}
isLoading={composerTriggerMenu.isLoading}
onSelect={composerTriggerMenu.onSelect}
/>
</View>
</View>
) : null}
<View className="pb-1">{workspaceControls}</View>

<ComposerSurface
Expand Down
Loading
Loading