Files
kandev/apps/web/hooks/use-task-create-prompt-mention.ts
T
João Salavisa a34ea719df feat(web): allow custom prompts in task creation input (#1155)
* refactor(web): extract measureCaretRect into shared util

Move the textarea caret-position measurement helper out of
rich-text-input.tsx into apps/web/lib/utils/caret-position.ts so the
task-create dialog's autocomplete wiring can reuse it without depending
on the chat-only RichTextInput component.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(web): allow custom prompts in task creation input

Add an @-mention autocomplete to the task-create dialog's prompt
textarea so users can quickly insert one of their saved custom prompts
(built-in + user-created) by typing @<name>.

- Extend useInlineMention with a promptInsertMode option. The default
  "context" mode preserves chat behavior (attach the prompt to the
  context store). The new "inline" mode replaces the @query text with
  the prompt's full content, requiring no backend changes -- the
  resolved text just becomes part of the task description on submit.
- Wire the hook into TaskFormInputs via a small useTaskCreatePromptMention
  helper that adapts the existing textarea ref to the RichTextInputHandle
  surface the hook expects (focus / selection / caret-rect / value).
- Reuse the existing MentionMenu popup component for parity with chat.
- Cover the new branches with unit tests for makePromptItem /
  detectMentionTrigger / filterItems, and an E2E spec exercising
  the menu open / inline-insertion / Enter-doesn't-submit /
  trigger-inside-word paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(web): make mention popup clickable inside Radix Dialog

Radix Dialog sets pointer-events: none on document.body to enforce
modal isolation. The mention/slash menu portals to document.body and
inherits the disabled pointer events when opened from inside a Dialog
(the task-create flow). The popup was visible but unclickable: keyboard
select worked, mouse clicks were swallowed by the Dialog body.

Restore pointerEvents: auto on the popup container so it stays
clickable when invoked from within a modal. Bump z-index to 60 so the
popup also wins the visual stacking race against the Dialog (overlay +
content are both z-50). Chat continues to work unchanged since z-60 and
pointer-events: auto are no-ops outside a modal.

Add an adversarial QA spec covering mouse-click select, ArrowDown +
Enter on the 2nd item, Escape close, multi-line content auto-grow,
backspace-over-@ close, space-after-@ close, no-arrow-cursor-shift,
and end-to-end submit. The mouse-click case is the one this fix
unblocks; the rest pin behavior for future regressions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(web): address PR review feedback

- caret-position.ts: wrap DOM measurement in try/finally so the mirror
  element is always removed even if getBoundingClientRect throws
- useTextareaHandlers: destructure stable fn refs from mention before
  useCallback so deps are specific properties, not the whole object —
  avoids defeating memoization on every keystroke
- QA spec: consolidate inline cleanupPrompts calls into a shared
  test.afterEach so cleanup always runs even when a test throws; update
  submit test comment to accurately reflect what is asserted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): tighten ArrowDown selection assertion in QA E2E spec

Equal filter scores sort by insertion order (stable), so qa-arr-1 lands
at index 0 and qa-arr-2 at index 1. After one ArrowDown from the default
the selected item is "SECOND" — assert that directly instead of accepting
either value, matching the test title "selects the second prompt".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:52:39 +01:00

61 lines
2.1 KiB
TypeScript

"use client";
import { useLayoutEffect, useMemo, useRef } from "react";
import { useInlineMention } from "@/hooks/use-inline-mention";
import { measureCaretRect } from "@/lib/utils/caret-position";
import type { RichTextInputHandle } from "@/components/task/chat/rich-text-input";
type Params = {
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
value: string;
onChange: (newValue: string) => void;
};
/**
* Wires the `useInlineMention` autocomplete hook to a plain `<textarea>` so
* the task-create prompt input can offer `@`-mention selection of saved
* custom prompts. Inlines the prompt's content text at the trigger position
* (no context chips, no backend changes).
*/
export function useTaskCreatePromptMention({ textareaRef, value, onChange }: Params) {
const adapter = useMemo<RichTextInputHandle>(
() => ({
focus: () => textareaRef.current?.focus(),
blur: () => textareaRef.current?.blur(),
getSelectionStart: () => textareaRef.current?.selectionStart ?? 0,
getSelectionEnd: () => textareaRef.current?.selectionEnd ?? 0,
setSelectionRange: (start: number, end: number) => {
textareaRef.current?.setSelectionRange(start, end);
},
getCaretRect: () => {
const ta = textareaRef.current;
return ta ? measureCaretRect(ta, ta.value) : null;
},
getValue: () => textareaRef.current?.value ?? "",
setValue: (v: string) => onChange(v),
insertText: (text: string, from: number, to: number) => {
const current = textareaRef.current?.value ?? "";
const newValue = current.substring(0, from) + text + current.substring(to);
onChange(newValue);
requestAnimationFrame(() => {
textareaRef.current?.setSelectionRange(from + text.length, from + text.length);
});
},
getTextareaElement: () => textareaRef.current,
}),
[textareaRef, onChange],
);
const adapterRef = useRef<RichTextInputHandle | null>(adapter);
useLayoutEffect(() => {
adapterRef.current = adapter;
}, [adapter]);
return useInlineMention({
inputRef: adapterRef,
value,
onChange,
promptInsertMode: "inline",
});
}