Skip to content

Commit dfeaf87

Browse files
committed
fix(home): clear the composer when a result is handed to the Assistant, follow the search query in a chat, restore a queued message's mode, and seed the base list
1 parent 6da5af5 commit dfeaf87

6 files changed

Lines changed: 105 additions & 33 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export function KnowledgeSearchResults({
231231
if (!basesPending && knowledgeBaseIds.length === 0) {
232232
return (
233233
<p className='px-2 py-2 text-[var(--text-muted)] text-caption'>
234-
Nothing to search yet. Connect a source above to index what you can open.
234+
Nothing to search yet. Clear the query and connect a source to index what you can open.
235235
</p>
236236
)
237237
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {
44
memo,
55
type ReactNode,
6+
type RefObject,
67
useCallback,
78
useDeferredValue,
89
useEffect,
@@ -63,6 +64,12 @@ interface MothershipChatProps {
6364
isSending: boolean
6465
/** The composer's Search-mode results, shown above the input. */
6566
searchResults?: ReactNode
67+
/** The live search query; the composer shows it so the box and the results never disagree. */
68+
searchQuery?: string
69+
/** The composer, for a caller that hands a question to the agent from outside the box. */
70+
userInputRef?: RefObject<UserInputHandle | null>
71+
/** Puts the composer in the mode a queued message was written in, when one is loaded for editing. */
72+
onRestoreQueuedMode?: (requestMode: QueuedMessage['requestMode']) => void
6673
isReconnecting?: boolean
6774
isLoading?: boolean
6875
onSubmit: (
@@ -319,6 +326,9 @@ export function MothershipChat({
319326
messages: messagesProp,
320327
isSending,
321328
searchResults,
329+
searchQuery,
330+
userInputRef: userInputRefProp,
331+
onRestoreQueuedMode,
322332
isReconnecting = false,
323333
isLoading = false,
324334
onSubmit,
@@ -663,7 +673,8 @@ export function MothershipChat({
663673
item.index !== lastIndex && item.start < (instance.scrollElement?.scrollTop ?? 0)
664674

665675
const scrolledChatRef = useRef<string | undefined | typeof UNSCROLLED>(UNSCROLLED)
666-
const userInputRef = useRef<UserInputHandle>(null)
676+
const ownUserInputRef = useRef<UserInputHandle>(null)
677+
const userInputRef = userInputRefProp ?? ownUserInputRef
667678
const messageQueueRef = useRef(messageQueue)
668679
useEffect(() => {
669680
messageQueueRef.current = messageQueue
@@ -686,9 +697,11 @@ export function MothershipChat({
686697
const handleEditQueued = useCallback(
687698
(id: string) => {
688699
const msg = onEditQueuedMessage(id)
689-
if (msg) userInputRef.current?.loadQueuedMessage(msg)
700+
if (!msg) return
701+
onRestoreQueuedMode?.(msg.requestMode)
702+
userInputRef.current?.loadQueuedMessage(msg)
690703
},
691-
[onEditQueuedMessage]
704+
[onEditQueuedMessage, onRestoreQueuedMode, userInputRef]
692705
)
693706

694707
const handleEditQueuedTail = useCallback(() => {
@@ -831,6 +844,7 @@ export function MothershipChat({
831844
<UserInput
832845
key={draftScopeKey}
833846
ref={userInputRef}
847+
defaultValue={searchQuery}
834848
onSubmit={onSubmit}
835849
canSearch={canSearch}
836850
clearOnSubmit={clearOnSubmit}

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export interface UserInputHandle {
9696
* names chip with brand icons. Focuses the input and places the caret at the
9797
* end. Does NOT submit. Safe to call with the same text twice in a row. */
9898
populatePrompt: (text: string) => void
99+
/** Empties the composer and its draft, as a send does; for a question handed to the agent from outside the box. */
100+
clear: () => void
99101
}
100102

101103
/**
@@ -434,6 +436,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
434436
currentEditor.setContexts(msg.contexts ?? [])
435437
currentEditor.focusAtEnd()
436438
},
439+
clear: clearComposer,
437440
populatePrompt: (text: string) => {
438441
// `text` is a curated prompt, so opt its bare integration names into
439442
// `@`-mention form before chipification (the auto-mention pipeline only
@@ -551,6 +554,24 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
551554
textareaRef.current?.focus()
552555
}
553556

557+
/** Empties the text, chips, attachments, transcript, and the saved draft in one step. */
558+
const clearComposer = useCallback(() => {
559+
editorRef.current.clear()
560+
sttPrefixRef.current = ''
561+
if (draftSaveTimerRef.current !== null) {
562+
window.clearTimeout(draftSaveTimerRef.current)
563+
draftSaveTimerRef.current = null
564+
}
565+
pendingDraftRef.current = null
566+
if (draftScopeKeyRef.current) {
567+
useMothershipDraftsStore.getState().clearDraft(draftScopeKeyRef.current)
568+
}
569+
/** The chips are gone with the text, and clearing is not a removal to report. */
570+
prevSelectedContextsRef.current = []
571+
resetTranscript()
572+
filesRef.current.clearAttachedFiles()
573+
}, [resetTranscript])
574+
554575
const handleSubmit = useCallback(() => {
555576
const currentFiles = filesRef.current
556577
const currentEditor = editorRef.current
@@ -575,27 +596,13 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
575596
fileAttachmentsForApi.length > 0 ? fileAttachmentsForApi : undefined,
576597
activeContexts.length > 0 ? activeContexts : undefined
577598
)
578-
if (clearOnSubmitRef.current) {
579-
currentEditor.clear()
580-
sttPrefixRef.current = ''
581-
if (draftSaveTimerRef.current !== null) {
582-
window.clearTimeout(draftSaveTimerRef.current)
583-
draftSaveTimerRef.current = null
584-
}
585-
pendingDraftRef.current = null
586-
if (draftScopeKeyRef.current) {
587-
useMothershipDraftsStore.getState().clearDraft(draftScopeKeyRef.current)
588-
}
589-
/**
590-
* The chips are gone with the text, and clearing is not a removal to
591-
* report. A composer that keeps its text (Search mode) keeps its chips
592-
* too, so the diff base stays in step with what is still selected.
593-
*/
594-
prevSelectedContextsRef.current = []
595-
}
596-
resetTranscript()
597-
currentFiles.clearAttachedFiles()
598-
}, [onSubmit, resetTranscript])
599+
/**
600+
* A composer that keeps its text (Search mode) keeps its attachments and
601+
* chips too: the search took the query alone, and the person may hand the
602+
* rest to the agent next.
603+
*/
604+
if (clearOnSubmitRef.current) clearComposer()
605+
}, [onSubmit, clearComposer])
599606

600607
/**
601608
* Enter policy for the editor: mirror canSubmit's uploading guard (Enter

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import type {
8989
FileAttachmentForApi,
9090
MothershipResource,
9191
MothershipResourceType,
92+
QueuedMessage,
9293
WorkspaceResourceRef,
9394
} from './types'
9495

@@ -193,7 +194,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
193194
* box and the results never show two different queries.
194195
*/
195196
useEffect(() => {
196-
if (searchQuery && composerMode === 'build') void setComposerMode('search')
197+
if (searchQuery.trim() && composerMode === 'build') void setComposerMode('search')
197198
}, [searchQuery, composerMode, setComposerMode])
198199
/** The bases an Ask turn is grounded in; read through a ref so a list refresh never rebuilds the submit handler. */
199200
const { data: knowledgeBases = EMPTY_KNOWLEDGE_BASES } = useKnowledgeBasesQuery(workspaceId)
@@ -202,6 +203,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
202203
const hasCheckedLandingStorageRef = useRef(false)
203204
const initialViewInputRef = useRef<HTMLDivElement>(null)
204205
const initialViewUserInputRef = useRef<UserInputHandle>(null)
206+
const chatViewUserInputRef = useRef<UserInputHandle>(null)
205207

206208
const [isInputEntering, setIsInputEntering] = useState(false)
207209

@@ -499,6 +501,8 @@ export function Home({ chatId, userName, userId }: HomeProps) {
499501
const mode = modeOverride ?? composerMode
500502
const answering = mode === 'assistant'
501503
if (mode === 'search') {
504+
/** A search sends nothing, so an edit in progress is released rather than left waiting. */
505+
if (editingQueuedId) cancelQueueEdit()
502506
if (trimmed) setSearchQuery(trimmed)
503507
return
504508
}
@@ -525,23 +529,43 @@ export function Home({ chatId, userName, userId }: HomeProps) {
525529
workspaceId,
526530
chatId,
527531
composerMode,
532+
editingQueuedId,
533+
cancelQueueEdit,
528534
prepareResourceViewForAgentTurn,
529535
sendMessage,
530536
setSearchQuery,
531537
]
532538
)
533539

534-
/** An emptied search box returns to the sources; nothing else reads the cleared query. */
535-
const clearSearch = useCallback(() => setSearchQuery(''), [setSearchQuery])
540+
/**
541+
* A queued message re-enters the composer in the mode it was written in: an
542+
* Assistant question edits as an Assistant question, and never as a Search,
543+
* which submits nothing and would leave the edit stranded.
544+
*/
545+
const restoreQueuedMode = useCallback(
546+
(requestMode: QueuedMessage['requestMode']) => {
547+
void setComposerMode(requestMode === 'ask' ? 'assistant' : 'build')
548+
},
549+
[setComposerMode]
550+
)
551+
552+
/** An emptied search box returns to the sources; a send in any other mode has no search to clear. */
553+
const clearSearch = useCallback(() => {
554+
if (searchQueryValue !== null) setSearchQuery('')
555+
}, [searchQueryValue, setSearchQuery])
536556

537557
/**
538558
* Summarize or Answer on a result: switch to Assistant and hand the question
539559
* to it. The submit reads the mode from this render, so it is sent as an
540-
* Assistant turn directly rather than waiting for the URL to update.
560+
* Assistant turn directly rather than waiting for the URL to update, and the
561+
* box is emptied as a send empties it, so the query does not linger as a
562+
* draft under the answer.
541563
*/
542564
const handleSummarize = (prompt: string) => {
543565
void setComposerMode('assistant')
544566
setSearchQuery('')
567+
initialViewUserInputRef.current?.clear()
568+
chatViewUserInputRef.current?.clear()
545569
handleSubmit(prompt, undefined, undefined, 'assistant')
546570
}
547571
const showSearchResults = composerMode === 'search' && searchQuery.trim().length > 0
@@ -800,6 +824,9 @@ export function Home({ chatId, userName, userId }: HomeProps) {
800824
messages={messages}
801825
isSending={isSending}
802826
searchResults={searchResults}
827+
searchQuery={searchQuery}
828+
userInputRef={chatViewUserInputRef}
829+
onRestoreQueuedMode={restoreQueuedMode}
803830
isReconnecting={isReconnecting}
804831
isLoading={showChatSkeleton}
805832
onSubmit={handleSubmit}

apps/sim/app/workspace/[workspaceId]/home/prefetch.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import type { QueryClient } from '@tanstack/react-query'
2+
import { listKnowledgeBasesContract } from '@/lib/api/contracts/knowledge'
3+
import { internalSessionAuth } from '@/lib/api/server/routes'
4+
import { internalKnowledgePresenters } from '@/lib/knowledge/api/internal-route'
5+
import { listInternalKnowledgeBases } from '@/lib/knowledge/application/knowledge-bases'
26
import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context'
37
import { seedWorkspaceFiles } from '@/app/workspace/[workspaceId]/lib/seed-workspace-files'
8+
import { KNOWLEDGE_BASE_LIST_STALE_TIME, knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
49

510
/**
611
* Prefetches what the Home surface needs on top of the workspace layout's own prefetch.
712
*
813
* Home reads the workspace file list on mount (resource tabs, mentions, the resource picker), so
914
* the list is seeded by the routes that render Home rather than by the layout: seeding it in the
10-
* layout would pay for it on every workspace route, including the ones that never read it.
15+
* layout would pay for it on every workspace route, including the ones that never read it. The
16+
* knowledge-base list is seeded the same way, under the client hook's key and stale time: an
17+
* Assistant turn attaches the searched bases at submit, and a first question typed before the
18+
* list arrived would otherwise go out with nothing to search.
1119
*
1220
* The seed carries no authorization of its own, so the viewer is proved first. This reuses the
1321
* layout's `cache`d host-context lookup rather than re-deriving the permission, so it costs no
@@ -23,5 +31,21 @@ export async function prefetchHomeSurface(
2331
const hostContext = await getWorkspaceHostContextForViewer(workspaceId, userId)
2432
if (!hostContext) return
2533

26-
await seedWorkspaceFiles(queryClient, workspaceId)
34+
await Promise.all([
35+
seedWorkspaceFiles(queryClient, workspaceId),
36+
queryClient.prefetchQuery({
37+
queryKey: knowledgeKeys.list(workspaceId, 'active'),
38+
queryFn: async () => {
39+
const principal = await internalSessionAuth.authenticate()
40+
const result = await listInternalKnowledgeBases.execute({
41+
principal,
42+
input: { workspaceId, scope: 'active' },
43+
})
44+
return listKnowledgeBasesContract.response.schema.parse(
45+
internalKnowledgePresenters.list(result)
46+
).data
47+
},
48+
staleTime: KNOWLEDGE_BASE_LIST_STALE_TIME,
49+
}),
50+
])
2751
}

apps/sim/app/workspace/[workspaceId]/home/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export interface FileAttachmentForApi {
2525

2626
/**
2727
* A request mode a send asks the agent for beyond the default. `ask` is an
28-
* answer drawn from the attached knowledge bases with the knowledge tool
29-
* alone: the server attaches no integration tools to the turn.
28+
* Assistant turn: an answer drawn from the attached knowledge bases first,
29+
* with a connected integration reached only when those cannot answer.
3030
*/
3131
export type ChatRequestMode = 'ask'
3232

0 commit comments

Comments
 (0)