| author | |
| committer | |
| log | 0cf482b08bca0249ec94fbcbd0eb04fff28d9b3d |
| tree | 9f9a402b19c4ada6aafd3e49d552f81b1cb09d43 |
| parent | 1905846e03226582f64fec16e1500ddffa151fdf |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Khai Shern, Toh <khaishern8235@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>6 files changed, 126 insertions(+), 6 deletions(-)
apps/web/src/components/ComposerPromptEditorTiptap.tsx+25-1| ... | @@ -121,6 +121,11 @@ export interface ComposerPromptEditorProps { | ... | @@ -121,6 +121,11 @@ export interface ComposerPromptEditorProps { |
| 121 | skills: ReadonlyArray<ServerProviderSkill>; | 121 | skills: ReadonlyArray<ServerProviderSkill>; |
| 122 | disabled: boolean; | 122 | disabled: boolean; |
| 123 | placeholder: string; | 123 | placeholder: string; |
| 124 | ariaLabel?: string | undefined; | ||
| 125 | /** Identifies an editor with suggestions, even while its list is closed. */ | ||
| 126 | suggestionListId?: string | undefined; | ||
| 127 | /** References the highlighted option only while its list is rendered. */ | ||
| 128 | activeSuggestionId?: string | undefined; | ||
| 124 | containerClassName?: string; | 129 | containerClassName?: string; |
| 125 | className?: string; | 130 | className?: string; |
| 126 | placeholderClassName?: string; | 131 | placeholderClassName?: string; |
| ... | @@ -592,6 +597,9 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { | ... | @@ -592,6 +597,9 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { |
| 592 | skills, | 597 | skills, |
| 593 | disabled, | 598 | disabled, |
| 594 | placeholder, | 599 | placeholder, |
| 600 | ariaLabel, | ||
| 601 | suggestionListId, | ||
| 602 | activeSuggestionId, | ||
| 595 | containerClassName, | 603 | containerClassName, |
| 596 | className, | 604 | className, |
| 597 | placeholderClassName, | 605 | placeholderClassName, |
| ... | @@ -752,9 +760,25 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { | ... | @@ -752,9 +760,25 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { |
| 752 | ), | 760 | ), |
| 753 | "data-testid": "composer-editor", | 761 | "data-testid": "composer-editor", |
| 754 | "data-composer-rich-text": richText ? "true" : "false", | 762 | "data-composer-rich-text": richText ? "true" : "false", |
| 763 | role: "textbox", | ||
| 764 | "aria-multiline": "true", | ||
| 765 | ...(ariaLabel ? { "aria-label": ariaLabel } : {}), | ||
| 766 | ...(disabled ? { "aria-readonly": "true" } : {}), | ||
| 767 | ...(!disabled && suggestionListId | ||
| 768 | ? { | ||
| 769 | "aria-autocomplete": "list", | ||
| 770 | "aria-haspopup": "listbox", | ||
| 771 | ...(activeSuggestionId | ||
| 772 | ? { | ||
| 773 | "aria-controls": suggestionListId, | ||
| 774 | "aria-activedescendant": activeSuggestionId, | ||
| 775 | } | ||
| 776 | : {}), | ||
| 777 | } | ||
| 778 | : {}), | ||
| 755 | "aria-placeholder": placeholder, | 779 | "aria-placeholder": placeholder, |
| 756 | }), | 780 | }), |
| 757 | [className, placeholder, richText], | 781 | [activeSuggestionId, ariaLabel, className, disabled, placeholder, richText, suggestionListId], |
| 758 | ); | 782 | ); |
| 759 | 783 | ||
| 760 | const editor = useEditor( | 784 | const editor = useEditor( |
apps/web/src/components/chat/ChatComposer.tsx+33-3| ... | @@ -58,6 +58,7 @@ import { | ... | @@ -58,6 +58,7 @@ import { |
| 58 | type ReactNode, | 58 | type ReactNode, |
| 59 | useCallback, | 59 | useCallback, |
| 60 | useEffect, | 60 | useEffect, |
| 61 | useId, | ||
| 61 | useImperativeHandle, | 62 | useImperativeHandle, |
| 62 | useLayoutEffect, | 63 | useLayoutEffect, |
| 63 | useMemo, | 64 | useMemo, |
| ... | @@ -245,7 +246,11 @@ import { useEnvironmentQuery } from "~/state/query"; | ... | @@ -245,7 +246,11 @@ import { useEnvironmentQuery } from "~/state/query"; |
| 245 | import { useDebouncedValue } from "~/state/queries"; | 246 | import { useDebouncedValue } from "~/state/queries"; |
| 246 | import { ProviderModelPicker } from "./ProviderModelPicker"; | 247 | import { ProviderModelPicker } from "./ProviderModelPicker"; |
| 247 | import { resolveModelPickerSelectedModel } from "./ModelPickerContent"; | 248 | import { resolveModelPickerSelectedModel } from "./ModelPickerContent"; |
| 248 | import { type ComposerCommandItem, ComposerCommandMenu } from "./ComposerCommandMenu"; | 249 | import { |
| 250 | type ComposerCommandItem, | ||
| 251 | ComposerCommandMenu, | ||
| 252 | composerSuggestionOptionId, | ||
| 253 | } from "./ComposerCommandMenu"; | ||
| 249 | import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions"; | 254 | import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions"; |
| 250 | import { CompactComposerControlsMenu } from "./CompactComposerControlsMenu"; | 255 | import { CompactComposerControlsMenu } from "./CompactComposerControlsMenu"; |
| 251 | import { ComposerImageThumbnail } from "./ComposerImageThumbnail"; | 256 | import { ComposerImageThumbnail } from "./ComposerImageThumbnail"; |
| ... | @@ -2108,6 +2113,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -2108,6 +2113,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2108 | resetTrigger: resetComposerTrigger, | 2113 | resetTrigger: resetComposerTrigger, |
| 2109 | } = useComposerTriggerState(() => detectComposerTrigger(prompt, prompt.length)); | 2114 | } = useComposerTriggerState(() => detectComposerTrigger(prompt, prompt.length)); |
| 2110 | const [composerHighlightedItemId, setComposerHighlightedItemId] = useState<string | null>(null); | 2115 | const [composerHighlightedItemId, setComposerHighlightedItemId] = useState<string | null>(null); |
| 2116 | const composerSuggestionId = useId(); | ||
| 2117 | const composerSuggestionListId = `${composerSuggestionId}-${encodeURIComponent(draftId ?? activeThreadId ?? "new")}-suggestions`; | ||
| 2111 | // Active ArrowUp recall. Cleared on edit and on thread switch. | 2118 | // Active ArrowUp recall. Cleared on edit and on thread switch. |
| 2112 | const promptHistoryPositionRef = useRef<ComposerPromptHistoryPosition | null>(null); | 2119 | const promptHistoryPositionRef = useRef<ComposerPromptHistoryPosition | null>(null); |
| 2113 | const [composerHighlightedSearchKey, setComposerHighlightedSearchKey] = useState<string | null>( | 2120 | const [composerHighlightedSearchKey, setComposerHighlightedSearchKey] = useState<string | null>( |
| ... | @@ -2502,7 +2509,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -2502,7 +2509,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2502 | 2509 | ||
| 2503 | const composerMenuOpen = Boolean(composerTrigger); | 2510 | const composerMenuOpen = Boolean(composerTrigger); |
| 2504 | const composerMenuSearchKey = composerTrigger | 2511 | const composerMenuSearchKey = composerTrigger |
| 2505 | ? `${composerTrigger.kind}:${composerTrigger.query.trim().toLowerCase()}` | 2512 | ? `${composerSuggestionListId}:${composerTrigger.kind}:${composerTrigger.query.trim().toLowerCase()}` |
| 2506 | : null; | 2513 | : null; |
| 2507 | const activeComposerMenuItem = useMemo(() => { | 2514 | const activeComposerMenuItem = useMemo(() => { |
| 2508 | const activeItemId = resolveComposerMenuActiveItemId({ | 2515 | const activeItemId = resolveComposerMenuActiveItemId({ |
| ... | @@ -2529,6 +2536,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -2529,6 +2536,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2529 | ); | 2536 | ); |
| 2530 | 2537 | ||
| 2531 | const isComposerApprovalState = activePendingApproval !== null; | 2538 | const isComposerApprovalState = activePendingApproval !== null; |
| 2539 | const composerSuggestionsVisible = composerMenuOpen && !isComposerApprovalState; | ||
| 2540 | const composerSuggestionListVisible = composerSuggestionsVisible && composerMenuItems.length > 0; | ||
| 2532 | const activePendingUserInput = pendingUserInputs[0] ?? null; | 2541 | const activePendingUserInput = pendingUserInputs[0] ?? null; |
| 2533 | const isChoiceOnlyPendingQuestion = | 2542 | const isChoiceOnlyPendingQuestion = |
| 2534 | activePendingProgress?.activeQuestion?.allowCustomAnswer === false; | 2543 | activePendingProgress?.activeQuestion?.allowCustomAnswer === false; |
| ... | @@ -3176,6 +3185,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -3176,6 +3185,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3176 | // ------------------------------------------------------------------ | 3185 | // ------------------------------------------------------------------ |
| 3177 | useEffect(() => { | 3186 | useEffect(() => { |
| 3178 | setComposerHighlightedItemId(null); | 3187 | setComposerHighlightedItemId(null); |
| 3188 | setComposerHighlightedSearchKey(null); | ||
| 3179 | setComposerSubmissionError(null); | 3189 | setComposerSubmissionError(null); |
| 3180 | setProviderInputSubmissionError(null); | 3190 | setProviderInputSubmissionError(null); |
| 3181 | setComposerCursor(collapseExpandedComposerCursor(promptRef.current, promptRef.current.length)); | 3191 | setComposerCursor(collapseExpandedComposerCursor(promptRef.current, promptRef.current.length)); |
| ... | @@ -6454,9 +6464,19 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -6454,9 +6464,19 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 6454 | </ComposerCommandMenuLayer> | 6464 | </ComposerCommandMenuLayer> |
| 6455 | )} | 6465 | )} |
| 6456 | 6466 | ||
| 6457 | {composerMenuOpen && !isComposerApprovalState && ( | 6467 | <div role="status" aria-atomic="true" className="sr-only"> |
| 6468 | {composerSuggestionsVisible && composerMenuItems.length === 0 | ||
| 6469 | ? isComposerMenuLoading | ||
| 6470 | ? composerTriggerKind === "pull-request" | ||
| 6471 | ? "Finding pull request..." | ||
| 6472 | : "Searching workspace files..." | ||
| 6473 | : composerMenuEmptyState | ||
| 6474 | : ""} | ||
| 6475 | </div> | ||
| 6476 | {composerSuggestionsVisible && ( | ||
| 6458 | <ComposerCommandMenuLayer anchor={composerMenuAnchor}> | 6477 | <ComposerCommandMenuLayer anchor={composerMenuAnchor}> |
| 6459 | <ComposerCommandMenu | 6478 | <ComposerCommandMenu |
| 6479 | listId={composerSuggestionListId} | ||
| 6460 | items={composerMenuItems} | 6480 | items={composerMenuItems} |
| 6461 | resolvedTheme={resolvedTheme} | 6481 | resolvedTheme={resolvedTheme} |
| 6462 | isLoading={isComposerMenuLoading} | 6482 | isLoading={isComposerMenuLoading} |
| ... | @@ -6845,6 +6865,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -6845,6 +6865,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 6845 | ) : null} | 6865 | ) : null} |
| 6846 | <ComposerContextActionsContext value={composerContextActions}> | 6866 | <ComposerContextActionsContext value={composerContextActions}> |
| 6847 | <ComposerPromptEditor | 6867 | <ComposerPromptEditor |
| 6868 | ariaLabel="Message" | ||
| 6869 | suggestionListId={composerSuggestionListId} | ||
| 6870 | activeSuggestionId={ | ||
| 6871 | composerSuggestionListVisible && activeComposerMenuItem | ||
| 6872 | ? composerSuggestionOptionId( | ||
| 6873 | composerSuggestionListId, | ||
| 6874 | activeComposerMenuItem.id, | ||
| 6875 | ) | ||
| 6876 | : undefined | ||
| 6877 | } | ||
| 6848 | editorRef={composerEditorRef} | 6878 | editorRef={composerEditorRef} |
| 6849 | richTextEnabled={settings.composerRichTextEnabled} | 6879 | richTextEnabled={settings.composerRichTextEnabled} |
| 6850 | value={ | 6880 | value={ |
apps/web/src/components/chat/ComposerCommandMenu.test.tsx+28-1| ... | @@ -2,12 +2,37 @@ import { renderToStaticMarkup } from "react-dom/server"; | ... | @@ -2,12 +2,37 @@ import { renderToStaticMarkup } from "react-dom/server"; |
| 2 | import { ProviderDriverKind } from "@t3tools/contracts"; | 2 | import { ProviderDriverKind } from "@t3tools/contracts"; |
| 3 | import { describe, expect, it } from "vite-plus/test"; | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 4 | 4 | ||
| 5 | import { ComposerCommandMenu } from "./ComposerCommandMenu"; | 5 | import { ComposerCommandMenu, composerSuggestionOptionId } from "./ComposerCommandMenu"; |
| 6 | |||
| 7 | describe("composerSuggestionOptionId", () => { | ||
| 8 | it("keeps whitespace, escape-like paths, and malformed UTF-16 distinct", () => { | ||
| 9 | const paths = [ | ||
| 10 | "docs/my file.md", | ||
| 11 | "docs/my_file.md", | ||
| 12 | "docs/my%20file.md", | ||
| 13 | "docs/my\tfile.md", | ||
| 14 | "docs/\ud800.md", | ||
| 15 | "docs/\ud801.md", | ||
| 16 | "docs/\udc00.md", | ||
| 17 | "docs/\ufffd.md", | ||
| 18 | "docs/\\ud800.md", | ||
| 19 | "docs/\ud83d\ude80.md", | ||
| 20 | ]; | ||
| 21 | const ids = paths.map((path) => composerSuggestionOptionId("suggestions", `path:file:${path}`)); | ||
| 22 | |||
| 23 | expect(new Set(ids).size).toBe(paths.length); | ||
| 24 | for (const id of ids) expect(id).not.toMatch(/\s|[\ud800-\udfff]/u); | ||
| 25 | expect(composerSuggestionOptionId("other-composer", paths[0]!)).not.toBe( | ||
| 26 | composerSuggestionOptionId("suggestions", paths[0]!), | ||
| 27 | ); | ||
| 28 | }); | ||
| 29 | }); | ||
| 6 | 30 | ||
| 7 | describe("ComposerCommandMenu", () => { | 31 | describe("ComposerCommandMenu", () => { |
| 8 | it("renders slash commands with their descriptions", () => { | 32 | it("renders slash commands with their descriptions", () => { |
| 9 | const markup = renderToStaticMarkup( | 33 | const markup = renderToStaticMarkup( |
| 10 | <ComposerCommandMenu | 34 | <ComposerCommandMenu |
| 35 | listId="test-suggestions" | ||
| 11 | items={[ | 36 | items={[ |
| 12 | { | 37 | { |
| 13 | id: "slash:model", | 38 | id: "slash:model", |
| ... | @@ -33,6 +58,7 @@ describe("ComposerCommandMenu", () => { | ... | @@ -33,6 +58,7 @@ describe("ComposerCommandMenu", () => { |
| 33 | it("shows the app source for an app skill", () => { | 58 | it("shows the app source for an app skill", () => { |
| 34 | const markup = renderToStaticMarkup( | 59 | const markup = renderToStaticMarkup( |
| 35 | <ComposerCommandMenu | 60 | <ComposerCommandMenu |
| 61 | listId="test-suggestions" | ||
| 36 | items={[ | 62 | items={[ |
| 37 | { | 63 | { |
| 38 | id: "skill:codex:browser", | 64 | id: "skill:codex:browser", |
| ... | @@ -67,6 +93,7 @@ describe("ComposerCommandMenu", () => { | ... | @@ -67,6 +93,7 @@ describe("ComposerCommandMenu", () => { |
| 67 | it("shows the repo source for a slash skill", () => { | 93 | it("shows the repo source for a slash skill", () => { |
| 68 | const markup = renderToStaticMarkup( | 94 | const markup = renderToStaticMarkup( |
| 69 | <ComposerCommandMenu | 95 | <ComposerCommandMenu |
| 96 | listId="test-suggestions" | ||
| 70 | items={[ | 97 | items={[ |
| 71 | { | 98 | { |
| 72 | id: "skill:codex:ask-matt", | 99 | id: "skill:codex:ask-matt", |
apps/web/src/components/chat/ComposerCommandMenu.tsx+22-1| ... | @@ -69,6 +69,7 @@ export type ComposerCommandItem = | ... | @@ -69,6 +69,7 @@ export type ComposerCommandItem = |
| 69 | }; | 69 | }; |
| 70 | 70 | ||
| 71 | export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { | 71 | export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { |
| 72 | listId: string; | ||
| 72 | items: ComposerCommandItem[]; | 73 | items: ComposerCommandItem[]; |
| 73 | resolvedTheme: "light" | "dark"; | 74 | resolvedTheme: "light" | "dark"; |
| 74 | isLoading: boolean; | 75 | isLoading: boolean; |
| ... | @@ -104,11 +105,16 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { | ... | @@ -104,11 +105,16 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { |
| 104 | data-composer-command-drawer="true" | 105 | data-composer-command-drawer="true" |
| 105 | > | 106 | > |
| 106 | {props.items.length > 0 ? ( | 107 | {props.items.length > 0 ? ( |
| 107 | <CommandList className="max-h-72 min-h-0 scroll-pb-6"> | 108 | <CommandList |
| 109 | id={props.listId} | ||
| 110 | aria-label={props.triggerKind ? LISTBOX_LABEL_BY_TRIGGER[props.triggerKind] : undefined} | ||
| 111 | className="max-h-72 min-h-0 scroll-pb-6" | ||
| 112 | > | ||
| 108 | <CommandGroup> | 113 | <CommandGroup> |
| 109 | {props.items.map((item) => ( | 114 | {props.items.map((item) => ( |
| 110 | <ComposerCommandMenuItem | 115 | <ComposerCommandMenuItem |
| 111 | key={item.id} | 116 | key={item.id} |
| 117 | optionId={composerSuggestionOptionId(props.listId, item.id)} | ||
| 112 | item={item} | 118 | item={item} |
| 113 | triggerKind={props.triggerKind} | 119 | triggerKind={props.triggerKind} |
| 114 | resolvedTheme={props.resolvedTheme} | 120 | resolvedTheme={props.resolvedTheme} |
| ... | @@ -143,6 +149,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { | ... | @@ -143,6 +149,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { |
| 143 | }); | 149 | }); |
| 144 | 150 | ||
| 145 | const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { | 151 | const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { |
| 152 | optionId: string; | ||
| 146 | item: ComposerCommandItem; | 153 | item: ComposerCommandItem; |
| 147 | triggerKind: ComposerTriggerKind | null; | 154 | triggerKind: ComposerTriggerKind | null; |
| 148 | resolvedTheme: "light" | "dark"; | 155 | resolvedTheme: "light" | "dark"; |
| ... | @@ -159,6 +166,8 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { | ... | @@ -159,6 +166,8 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { |
| 159 | 166 | ||
| 160 | return ( | 167 | return ( |
| 161 | <CommandItem | 168 | <CommandItem |
| 169 | render={<div id={props.optionId} />} | ||
| 170 | aria-selected={props.isActive} | ||
| 162 | value={props.item.id} | 171 | value={props.item.id} |
| 163 | data-composer-item-id={props.item.id} | 172 | data-composer-item-id={props.item.id} |
| 164 | active={props.isActive} | 173 | active={props.isActive} |
| ... | @@ -211,6 +220,18 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { | ... | @@ -211,6 +220,18 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { |
| 211 | ); | 220 | ); |
| 212 | }); | 221 | }); |
| 213 | 222 | ||
| 223 | export function composerSuggestionOptionId(listId: string, itemId: string): string { | ||
| 224 | // JSON escapes lone UTF-16 surrogates before URI encoding without losing identity. | ||
| 225 | return `${listId}-${encodeURIComponent(JSON.stringify(itemId))}`; | ||
| 226 | } | ||
| 227 | |||
| 228 | const LISTBOX_LABEL_BY_TRIGGER: Record<ComposerTriggerKind, string> = { | ||
| 229 | path: "Files and folders", | ||
| 230 | "pull-request": "Pull requests", | ||
| 231 | "slash-command": "Commands", | ||
| 232 | skill: "Skills", | ||
| 233 | }; | ||
| 234 | |||
| 214 | const SKILL_SOURCE_ICON_BY_KIND: Record<ProviderSkillSourceKind, LucideIcon> = { | 235 | const SKILL_SOURCE_ICON_BY_KIND: Record<ProviderSkillSourceKind, LucideIcon> = { |
| 215 | app: BlocksIcon, | 236 | app: BlocksIcon, |
| 216 | repo: FolderIcon, | 237 | repo: FolderIcon, |
apps/web/src/components/chat/composerMenuHighlight.test.ts+17| ... | @@ -48,4 +48,21 @@ describe("resolveComposerMenuActiveItemId", () => { | ... | @@ -48,4 +48,21 @@ describe("resolveComposerMenuActiveItemId", () => { |
| 48 | }), | 48 | }), |
| 49 | ).toBe("top"); | 49 | ).toBe("top"); |
| 50 | }); | 50 | }); |
| 51 | |||
| 52 | it("clears the active result while async results are empty and resolves against restored results", () => { | ||
| 53 | const search = { | ||
| 54 | highlightedItemId: "second", | ||
| 55 | currentSearchKey: "path:src", | ||
| 56 | highlightedSearchKey: "path:src", | ||
| 57 | }; | ||
| 58 | const cleared = resolveComposerMenuActiveItemId({ ...search, items: [] }); | ||
| 59 | expect(cleared).toBeNull(); | ||
| 60 | expect( | ||
| 61 | resolveComposerMenuActiveItemId({ | ||
| 62 | ...search, | ||
| 63 | highlightedItemId: cleared, | ||
| 64 | items: [{ id: "new-result" }, { id: "second" }], | ||
| 65 | }), | ||
| 66 | ).toBe("new-result"); | ||
| 67 | }); | ||
| 51 | }); | 68 | }); |
apps/web/src/components/settings/SettingsFontPreviews.tsx+1| ... | @@ -39,6 +39,7 @@ export function PromptFontPreview() { | ... | @@ -39,6 +39,7 @@ export function PromptFontPreview() { |
| 39 | return ( | 39 | return ( |
| 40 | <div className="mt-1 mb-2 rounded-lg border border-border bg-background px-3 py-2"> | 40 | <div className="mt-1 mb-2 rounded-lg border border-border bg-background px-3 py-2"> |
| 41 | <ComposerPromptEditor | 41 | <ComposerPromptEditor |
| 42 | ariaLabel="Prompt font preview" | ||
| 42 | editorRef={editorRef} | 43 | editorRef={editorRef} |
| 43 | value={prompt} | 44 | value={prompt} |
| 44 | cursor={cursor} | 45 | cursor={cursor} |