a34ea719df
* 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>
61 lines
2.1 KiB
TypeScript
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",
|
|
});
|
|
}
|