@@ -2,13 +2,12 @@ import "./message-editor.css";
22import { ModelSelector } from "@features/sessions/components/ModelSelector" ;
33import { ArrowUp , Paperclip , Stop } from "@phosphor-icons/react" ;
44import { Flex , IconButton , Text , Tooltip } from "@radix-ui/themes" ;
5- import type { JSONContent } from "@tiptap/core" ;
6- import { EditorContent } from "@tiptap/react" ;
75import { forwardRef , useImperativeHandle , useRef } from "react" ;
86import {
9- createEditorHandle ,
10- useMessageEditor ,
11- } from "../hooks/useMessageEditor" ;
7+ type EditorContent ,
8+ type MentionChip ,
9+ useContenteditableEditor ,
10+ } from "../hooks/useContenteditableEditor" ;
1211import { useMessageEditorStore } from "../stores/messageEditorStore" ;
1312import { SuggestionPortal } from "./SuggestionPortal" ;
1413
@@ -17,7 +16,7 @@ export interface MessageEditorHandle {
1716 blur : ( ) => void ;
1817 clear : ( ) => void ;
1918 isEmpty : ( ) => boolean ;
20- getContent : ( ) => JSONContent | undefined ;
19+ getContent : ( ) => EditorContent ;
2120 getText : ( ) => string ;
2221}
2322
@@ -39,7 +38,7 @@ export const MessageEditor = forwardRef<
3938 (
4039 {
4140 sessionId,
42- placeholder,
41+ placeholder = "Type a message... @ to mention files, / for commands" ,
4342 onSubmit,
4443 onBashCommand,
4544 onBashModeChange,
@@ -50,15 +49,30 @@ export const MessageEditor = forwardRef<
5049 ref ,
5150 ) => {
5251 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
53- const actions = useMessageEditorStore ( ( s ) => s . actions ) ;
5452 const context = useMessageEditorStore ( ( s ) => s . contexts [ sessionId ] ) ;
5553 const taskId = context ?. taskId ;
5654 const disabled = context ?. disabled ?? false ;
5755 const isLoading = context ?. isLoading ?? false ;
5856 const isCloud = context ?. isCloud ?? false ;
5957 const repoPath = context ?. repoPath ;
6058
61- const { editor, isEmpty, isBashMode, submit } = useMessageEditor ( {
59+ const {
60+ editorRef,
61+ isEmpty,
62+ isBashMode,
63+ submit,
64+ focus,
65+ blur,
66+ clear,
67+ getText,
68+ getContent,
69+ insertChip,
70+ onInput,
71+ onKeyDown,
72+ onPaste,
73+ onCompositionStart,
74+ onCompositionEnd,
75+ } = useContenteditableEditor ( {
6276 sessionId,
6377 taskId,
6478 placeholder,
@@ -73,27 +87,27 @@ export const MessageEditor = forwardRef<
7387
7488 useImperativeHandle (
7589 ref ,
76- ( ) => createEditorHandle ( editor , sessionId , actions ) ,
77- [ editor , sessionId , actions ] ,
90+ ( ) => ( {
91+ focus,
92+ blur,
93+ clear,
94+ isEmpty : ( ) => isEmpty ,
95+ getContent,
96+ getText,
97+ } ) ,
98+ [ focus , blur , clear , isEmpty , getContent , getText ] ,
7899 ) ;
79100
80101 const handleFileSelect = ( e : React . ChangeEvent < HTMLInputElement > ) => {
81102 const files = e . target . files ;
82103 if ( files && files . length > 0 ) {
83104 for ( const file of Array . from ( files ) ) {
84- editor
85- ?. chain ( )
86- . focus ( )
87- . insertContent ( {
88- type : "mention" ,
89- attrs : {
90- id : file . name ,
91- label : file . name ,
92- type : "file" ,
93- } ,
94- } )
95- . insertContent ( " " )
96- . run ( ) ;
105+ const chip : MentionChip = {
106+ type : "file" ,
107+ id : file . name ,
108+ label : file . name ,
109+ } ;
110+ insertChip ( chip ) ;
97111 }
98112 onAttachFiles ?.( Array . from ( files ) ) ;
99113 }
@@ -105,7 +119,7 @@ export const MessageEditor = forwardRef<
105119 const handleContainerClick = ( e : React . MouseEvent ) => {
106120 const target = e . target as HTMLElement ;
107121 if ( ! target . closest ( "button" ) ) {
108- editor ?. commands . focus ( ) ;
122+ focus ( ) ;
109123 }
110124 } ;
111125
@@ -117,7 +131,24 @@ export const MessageEditor = forwardRef<
117131 style = { { cursor : "text" } }
118132 >
119133 < div className = "max-h-[200px] min-h-[30px] flex-1 overflow-y-auto font-mono text-sm" >
120- < EditorContent editor = { editor } />
134+ { /* biome-ignore lint/a11y/useSemanticElements: contenteditable is intentional for rich mention chips */ }
135+ < div
136+ ref = { editorRef }
137+ className = "cli-editor outline-none"
138+ contentEditable = { ! disabled }
139+ suppressContentEditableWarning
140+ spellCheck = { false }
141+ role = "textbox"
142+ tabIndex = { disabled ? - 1 : 0 }
143+ aria-multiline = "true"
144+ aria-placeholder = { placeholder }
145+ data-placeholder = { placeholder }
146+ onInput = { onInput }
147+ onKeyDown = { onKeyDown }
148+ onPaste = { onPaste }
149+ onCompositionStart = { onCompositionStart }
150+ onCompositionEnd = { onCompositionEnd }
151+ />
121152 </ div >
122153
123154 < SuggestionPortal sessionId = { sessionId } />
0 commit comments