33import { useEffect , useRef } from 'react'
44import { Chip , ComposerActionButton , cn , Tooltip , toast } from '@sim/emcn'
55import { ArrowUp , Paperclip , Plus , Search , Slash , StopFilled } from '@sim/emcn/icons'
6- import { escapeRegExp } from '@sim/utils/string'
76import { useQueries } from '@tanstack/react-query'
87import {
98 ASSISTANT_IMAGE_ACCEPT_ATTRIBUTE ,
@@ -25,9 +24,9 @@ import {
2524} from '@/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor'
2625import { organizationSkillOptions } from '@/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/organization-skill-options'
2726import { useConversationModeShortcut } from '@/app/workspace/[workspaceId]/home/components/user-input/hooks/use-conversation-mode-shortcut'
28- import type { ChatRequestMode } from '@/app/workspace/[workspaceId]/home/types'
27+ import { usePromptHistory } from '@/app/workspace/[workspaceId]/home/components/user-input/hooks/use-prompt-history'
28+ import type { ChatMessage , ChatRequestMode } from '@/app/workspace/[workspaceId]/home/types'
2929import type { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
30- import { SKILL_CHIP_TRIGGER } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
3130import { getSkillsQueryOptions } from '@/hooks/queries/skills'
3231import { useWorkspacesQuery } from '@/hooks/queries/workspace'
3332import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
@@ -49,6 +48,8 @@ interface ComposerProps {
4948 restoredContexts ?: ChatContext [ ]
5049 onSubmit : ( text : string , contexts ?: ChatContext [ ] ) => void
5150 onSendQueuedHead ?: ( ) => void
51+ onEditQueuedTail ?: ( ) => boolean
52+ messages ?: readonly ChatMessage [ ]
5253 onStop : ( ) => void
5354}
5455
@@ -69,6 +70,8 @@ export function Composer({
6970 onChange,
7071 onSubmit,
7172 onSendQueuedHead,
73+ onEditQueuedTail,
74+ messages,
7275 restoredContexts,
7376 onStop,
7477} : ComposerProps ) {
@@ -100,6 +103,12 @@ export function Composer({
100103 initialContexts : restoredContexts ,
101104 onPasteFiles : files . processFiles ,
102105 } )
106+ const history = usePromptHistory ( {
107+ editor,
108+ attachedFiles : files . attachedFiles ,
109+ restoreAttachedFiles : files . restoreAttachedFiles ,
110+ getMessages : ( ) => messages ?? [ ] ,
111+ } )
103112 const { textareaRef } = editor
104113 const searchBlocked =
105114 editor . getActiveContexts ( ) . length > 0 ||
@@ -143,19 +152,9 @@ export function Composer({
143152 } , [ editor . value , editor . contexts , onChange ] )
144153 useEffect ( ( ) => {
145154 if ( ! restoredContexts ) return
146- // A queued skill may belong to a workspace whose picker has never opened here.
147- // Restore its existing chip from the saved context, without rediscovering it.
148- let restoredText = editorRef . current . getValue ( )
149- for ( const context of restoredContexts ) {
150- if ( context . kind !== 'skill' ) continue
151- restoredText = restoredText . replace (
152- new RegExp ( `(^|\\s)/${ escapeRegExp ( context . label ) } (?=\\s|$)` , 'g' ) ,
153- `$1${ SKILL_CHIP_TRIGGER } ${ context . label } `
154- )
155- }
156- editorRef . current . setValue ( restoredText , { chipify : false } )
157- editorRef . current . setContexts ( restoredContexts )
158- } , [ restoredContexts ] )
155+ history . reset ( )
156+ editorRef . current . restorePrompt ( editorRef . current . getPlainValue ( ) , restoredContexts )
157+ } , [ restoredContexts , history . reset ] )
159158 useChatInputFocus ( { textareaRef } )
160159 const voice = useVoiceInput ( {
161160 organizationId : organization . id ,
@@ -184,6 +183,7 @@ export function Composer({
184183 voice . resetTranscript ( )
185184 const contexts = imagesOnly ? [ ] : editor . getActiveContexts ( )
186185 onSubmit ( editor . getPlainValue ( ) , contexts . length ? contexts : undefined )
186+ history . reset ( )
187187 editor . clear ( )
188188 }
189189
@@ -207,12 +207,18 @@ export function Composer({
207207 </ Tooltip . Root >
208208 )
209209
210+ const recallQueuedTail = ( ) => {
211+ if ( attachedFilesRef . current . length > 0 ) return false
212+ return onEditQueuedTail ?.( ) ?? false
213+ }
210214 const promptEditor = (
211215 < PromptEditor
212216 editor = { editor }
213217 placeholder = { placeholder }
214218 aria-label = 'Ask Sim'
215219 onSubmit = { submit }
220+ onNavigateHistory = { history . navigate }
221+ onArrowUpOnEmpty = { recallQueuedTail }
216222 className = { cn (
217223 'max-h-[200px] max-md:max-h-[min(200px,calc(var(--mobile-viewport-height,100dvh)*0.25))]' ,
218224 isInitialView && 'min-h-[56px] max-md:min-h-0'
@@ -308,6 +314,12 @@ export function Composer({
308314 onChange = { ( text ) => editor . setValue ( text ) }
309315 onSubmit = { submit }
310316 onPaste = { editor . handlePaste }
317+ onKeyDown = { ( event ) =>
318+ editor . handleKeyDown ( event , {
319+ onNavigateHistory : history . navigate ,
320+ onArrowUpOnEmpty : recallQueuedTail ,
321+ } )
322+ }
311323 placeholder = { placeholder }
312324 aria-label = 'Ask Sim'
313325 leadingControls = { leadingControls }
0 commit comments