Skip to content

fix(mobile): support /, $ and @ in the new-task composer - #7523

Open
kirill-markin wants to merge 1 commit into
pingdotgg:mainfrom
kirill-markin:fix/mobile-new-task-composer-triggers
Open

fix(mobile): support /, $ and @ in the new-task composer#7523
kirill-markin wants to merge 1 commit into
pingdotgg:mainfrom
kirill-markin:fix/mobile-new-task-composer-triggers

Conversation

@kirill-markin

@kirill-markin kirill-markin commented Aug 19, 2026

Copy link
Copy Markdown

Problem

On mobile, the composer on the new-task screen mounted a bare editor, so the very first message of a thread could not invoke a skill ($), a slash command (/), or a file mention (@). The trigger menu lived only in ThreadComposer, which is reachable once the thread exists. In practice you had to send a throwaway first message, wait for the thread, and only then invoke the skill in message two — which is exactly when the skill matters least.

Reported in #7498.

Fix

Extracted the trigger and menu logic out of ThreadComposer into a shared pure module plus a hook, and mounted ComposerCommandPopover on the new-task screen as the second call site.

  • composer-trigger-menu.ts — pure menu building, insertion semantics, and the caret rule, with a colocated *.test.ts (matches the local convention of new-task-project-selection.ts, thread-settings-options.ts).
  • use-composer-trigger-menu.ts — the React wiring: caret state, trigger detection, path search, memoized items, select handler.
  • ThreadComposer.tsx loses ~240 lines and keeps its previous behavior exactly; it passes no resetKey, so the new caret-reset branch never runs there.
  • NewTaskDraftScreen.tsx mounts the popover and passes resetKey: flow.draftKey, because that screen stays mounted across draft switches — without it a carried-over caret offset can open the popover with no user input.

Path search is scoped to the directory the task will actually run in (selectedWorktreePath || workspaceRoot in local mode, workspaceRoot in worktree mode), mirroring handleStart, so @ never searches the server's startup directory.

About the size:XL label

The label counts lines, not new behavior. The breakdown:

File Lines What it is
composer-trigger-menu.test.ts +313 tests for the extracted pure logic
composer-trigger-menu.ts +251 extraction — menu building and insertion semantics moved out of ThreadComposer
use-composer-trigger-menu.ts +125 extraction — the React wiring moved out of ThreadComposer
ThreadComposer.tsx +86 / -244 now calls the shared code; net -158
NewTaskDraftScreen.tsx +60 the actual new behavior: mount the popover, wire selection, pass the draft key

So ~376 lines are code moved verbatim out of ThreadComposer (which shrinks by 244), 313 are tests, and the new surface itself is ~60 lines. Happy to split the extraction and the new call site into two PRs if that reviews better, though the line count would be the same across the pair.

Before / after

Same screen, same input $d, Android emulator, identical disposable environment.

Before After

@ mentions on the same screen, scoped to the selected project:

Verification

  • vp test run apps/mobile/src/features/threads/composer-trigger-menu.test.ts — 20/20 green.
  • tsc --noEmit in apps/mobile — clean.
  • Android emulator, debug build, against a disposable backend with a seeded project: $d lists provider skills, @src lists project files, and tapping a row inserts [greet.ts](src/greet.ts). That last one also settles the one thing a static read cannot: the popover both renders and receives taps through the zero-height anchor inside KeyboardStickyView.

Deliberately out of scope

  • /model on the draft screen. On mobile the /model row only inserts literal text: nothing under apps/mobile/src consumes the slash-model trigger it becomes. Adding a real model picker to the trigger menu is a separate feature, so the draft screen omits the row.
  • ThreadDetailScreen.tsx:731. An existing thread resolves @ mentions against the project root even when it runs in a worktree. Pre-existing, untouched here, but the two surfaces now resolve mention cwd by different rules — worth a deliberate call rather than drift.
  • apps/swift-ios. The Swift rebuild already wires slashCommands, skills and searchPaths into its new-thread composer, so this gap does not exist there. This PR is for the app that ships today and for Android, which keeps this code either way. Happy to close it if you would rather let the rewrite carry it.

Model: Claude Opus 5 (1M context), harness: Claude Code.


Note

Low Risk
Localized mobile composer UX refactor with preserved thread behavior, explicit per-screen command gating, and unit tests on the extracted logic; no auth, payments, or server changes.

Overview
Brings the thread composer’s $ / / / @ trigger popover to new-task draft so the first message can use skills, slash commands, and file mentions without starting a thread first.

Refactor: Inline trigger/caret/menu logic is pulled out of ThreadComposer into shared composer-trigger-menu.ts (menu building, insertion, caret rules) plus useComposerTriggerMenu (caret state, detection, path search, select handler). ThreadComposer delegates to the hook and should behave the same; tests cover the pure helpers.

New-task behavior: NewTaskDraftScreen mounts ComposerCommandPopover, wires controlled selection, and scopes @ search to the directory the task will run in (worktree vs local). Built-ins are plan / default only when plan mode is on—/model is omitted on this screen. resetKey: flow.draftKey re-parks the caret when switching drafts so a stale offset doesn’t open the menu; the popover is hidden while an incoming share locks the editor.

Reviewed by Cursor Bugbot for commit ce5d27c. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add /, $, and @ trigger menus to the mobile new-task composer

  • Extracts trigger menu logic from ThreadComposer into a shared useComposerTriggerMenu hook, then wires it into both ThreadComposer and NewTaskDraftScreen.
  • The hook detects / (slash commands), $ (skills), and @ (path mentions) triggers, builds ranked menu items via buildComposerMenuItems, and handles insertion and interaction-mode switching via resolveComposerCommandSelection.
  • Built-in commands shown in the new-task screen are limited to plan and default; model is only available in ThreadComposer.
  • Caret position is clamped and re-parked on draft switches to prevent stale selections from opening unintended popovers.
📊 Macroscope summarized ce5d27c. 4 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

The new-task screen mounted a bare editor, so the first message of a thread
could not invoke a skill, a slash command, or a file mention. The trigger menu
existed only in the thread composer, which meant sending a throwaway first
message and using the second one to reach a skill.

Extracts the trigger and menu logic out of ThreadComposer into a shared pure
module plus a hook, and mounts ComposerCommandPopover on the new-task screen as
well. Path search is scoped to the directory the task will actually run in. The
thread composer keeps its previous behavior exactly.

Fixes pingdotgg#7498

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e1c202d-e188-4e9d-b193-9184b876d2e1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size:XL 500-999 changed lines (additions + deletions). label Aug 19, 2026
!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.

@macroscopeapp

macroscopeapp Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds new trigger menu capability to the new-task draft composer and refactors shared logic into reusable modules. The introduction of new user-facing behavior (command/skill/path selection) warrants review. There is also an unresolved Medium-severity finding about missing loading/empty-state feedback in the popover.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@kirill-markin

Copy link
Copy Markdown
Author

Status, so this does not read as abandoned — the blocking finding is answered in the review thread: #7523 (comment)

Short version: the items.length > 0 guard that hides the popover's loading and empty state is not introduced here. ThreadComposer.tsx:725 already has it on main, so ComposerCommandPopover's emptyText branch (ComposerCommandPopover.tsx:89-91,177) was unreachable on mobile before this PR. Keeping the identical guard on both call sites is what makes the new surface behave exactly like the thread composer, which is the constraint this PR set for itself. Relaxing it is a deliberate UX change to both surfaces — a bare @ would start opening a popover with a spinner on the most-used surface — so I would rather send it as its own PR than bundle it here. Say the word and I will.

Everything else is green: Cursor Bugbot, Macroscope Effect Service Conventions, and the label checks. The Vercel marketing deploy fails with "Authorization required to deploy", which is the standard restriction on fork PRs rather than anything in this branch.

I also verified this locally instead of leaning on CI: 20/20 unit tests for the extracted logic, clean tsc --noEmit for apps/mobile, and a run on an Android emulator against a disposable backend where $ lists provider skills, @ lists project files, and tapping a row inserts the mention. Before/after images and a breakdown of the size:XL label are in the description.

@juliusmarminge

juliusmarminge commented Sep 1, 2026

Copy link
Copy Markdown
Member

Reopening: #8587 added the draft menus, but this PR also suppresses the nonfunctional /model row on drafts and resets selection synchronously when drafts change. Those details are not in the merged implementation. My initial closure missed that remaining scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants