Skip to content

Commit 917af6d

Browse files
committed
improvement(mothership): chat stability
1 parent fe5f809 commit 917af6d

File tree

5 files changed

+58
-46
lines changed

5 files changed

+58
-46
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,10 @@ export function UserInput({
101101
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
102102
if (e.key === 'Enter' && !e.shiftKey) {
103103
e.preventDefault()
104-
handleSubmit()
104+
if (!isSending) handleSubmit()
105105
}
106106
},
107-
[handleSubmit]
107+
[handleSubmit, isSending]
108108
)
109109

110110
const toggleListening = useCallback(() => {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export function Home({ chatId }: HomeProps = {}) {
145145
<div className='flex h-full min-w-0 flex-1 flex-col'>
146146
<div className='min-h-0 flex-1 overflow-y-auto px-[24px] py-[16px]'>
147147
<div className='mx-auto max-w-[640px] space-y-[16px]'>
148-
{messages.map((msg) => {
148+
{messages.map((msg, index) => {
149149
if (msg.role === 'user') {
150150
return (
151151
<div key={msg.id} className='flex justify-end'>
@@ -159,7 +159,8 @@ export function Home({ chatId }: HomeProps = {}) {
159159
}
160160

161161
const hasBlocks = msg.contentBlocks && msg.contentBlocks.length > 0
162-
const isThisStreaming = isSending && msg === messages[messages.length - 1]
162+
const isLastAssistant = msg.role === 'assistant' && index === messages.length - 1
163+
const isThisStreaming = isSending && isLastAssistant
163164

164165
if (!hasBlocks && !msg.content && isThisStreaming) {
165166
return (

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useEffect, useRef, useState } from 'react'
22
import { createLogger } from '@sim/logger'
33
import { useQueryClient } from '@tanstack/react-query'
4-
import { usePathname, useRouter } from 'next/navigation'
4+
import { usePathname } from 'next/navigation'
55
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
66
import { tableKeys } from '@/hooks/queries/tables'
77
import {
@@ -116,8 +116,6 @@ function getPayloadData(payload: SSEPayload): SSEPayloadData | undefined {
116116
export function useChat(workspaceId: string, initialChatId?: string): UseChatReturn {
117117
const pathname = usePathname()
118118
const queryClient = useQueryClient()
119-
const router = useRouter()
120-
const routerRef = useRef(router)
121119
const [messages, setMessages] = useState<ChatMessage[]>([])
122120
const [isSending, setIsSending] = useState(false)
123121
const [error, setError] = useState<string | null>(null)
@@ -131,10 +129,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
131129
const streamIdRef = useRef<string | undefined>(undefined)
132130
const sendingRef = useRef(false)
133131
const toolArgsMapRef = useRef<Map<string, Record<string, unknown>>>(new Map())
134-
135-
useEffect(() => {
136-
routerRef.current = router
137-
}, [router])
132+
const streamGenRef = useRef(0)
138133

139134
const isHomePage = pathname.endsWith('/home')
140135

@@ -157,6 +152,10 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
157152
}, [])
158153

159154
useEffect(() => {
155+
if (sendingRef.current) {
156+
chatIdRef.current = initialChatId
157+
return
158+
}
160159
chatIdRef.current = initialChatId
161160
appliedChatIdRef.current = undefined
162161
setMessages([])
@@ -168,10 +167,12 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
168167

169168
useEffect(() => {
170169
if (!isHomePage || !chatIdRef.current) return
170+
streamGenRef.current++
171171
chatIdRef.current = undefined
172172
appliedChatIdRef.current = undefined
173173
abortControllerRef.current?.abort()
174174
abortControllerRef.current = null
175+
sendingRef.current = false
175176
setMessages([])
176177
setError(null)
177178
setIsSending(false)
@@ -252,7 +253,11 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
252253
activeStreamId,
253254
})
254255
}
255-
routerRef.current.replace(`/workspace/${workspaceId}/task/${parsed.chatId}`)
256+
window.history.replaceState(
257+
null,
258+
'',
259+
`/workspace/${workspaceId}/task/${parsed.chatId}`
260+
)
256261
}
257262
}
258263
break
@@ -407,6 +412,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
407412
)
408413

409414
const finalize = useCallback(() => {
415+
sendingRef.current = false
410416
setIsSending(false)
411417
abortControllerRef.current = null
412418
chatBottomRef.current?.scrollIntoView({ behavior: 'smooth' })
@@ -422,8 +428,10 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
422428
const activeStreamId = chatHistory?.activeStreamId
423429
if (!activeStreamId || !appliedChatIdRef.current || sendingRef.current) return
424430

431+
const gen = ++streamGenRef.current
425432
const abortController = new AbortController()
426433
abortControllerRef.current = abortController
434+
sendingRef.current = true
427435
setIsSending(true)
428436

429437
const assistantId = crypto.randomUUID()
@@ -442,7 +450,9 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
442450
} catch (err) {
443451
if (err instanceof Error && err.name === 'AbortError') return
444452
} finally {
445-
finalize()
453+
if (streamGenRef.current === gen) {
454+
finalize()
455+
}
446456
}
447457
}
448458
reconnect()
@@ -468,6 +478,8 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
468478

469479
abortControllerRef.current?.abort()
470480

481+
const gen = ++streamGenRef.current
482+
471483
setError(null)
472484
setIsSending(true)
473485
sendingRef.current = true
@@ -530,16 +542,35 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
530542
if (err instanceof Error && err.name === 'AbortError') return
531543
setError(err instanceof Error ? err.message : 'Failed to send message')
532544
} finally {
533-
sendingRef.current = false
534-
finalize()
545+
if (streamGenRef.current === gen) {
546+
finalize()
547+
}
535548
}
536549
},
537550
[workspaceId, queryClient, processSSEStream, finalize]
538551
)
539552

540553
const stopGeneration = useCallback(() => {
554+
streamGenRef.current++
541555
abortControllerRef.current?.abort()
556+
abortControllerRef.current = null
557+
sendingRef.current = false
542558
setIsSending(false)
559+
560+
const activeChatId = chatIdRef.current
561+
if (activeChatId) {
562+
queryClient.invalidateQueries({ queryKey: taskKeys.detail(activeChatId) })
563+
}
564+
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
565+
}, [workspaceId, queryClient])
566+
567+
useEffect(() => {
568+
return () => {
569+
streamGenRef.current++
570+
abortControllerRef.current?.abort()
571+
abortControllerRef.current = null
572+
sendingRef.current = false
573+
}
543574
}, [])
544575

545576
return {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export function Table({
166166
const [renameValue, setRenameValue] = useState('')
167167
const [deletingColumn, setDeletingColumn] = useState<string | null>(null)
168168

169+
const containerRef = useRef<HTMLDivElement>(null)
169170
const isDraggingRef = useRef(false)
170171

171172
const { tableData, isLoadingTable, rows, isLoadingRows } = useTableData({
@@ -362,12 +363,6 @@ export function Table({
362363
const column = columnsRef.current.find((c) => c.name === columnName)
363364
if (!column) return
364365

365-
if (column.type === 'json') {
366-
const row = rowsRef.current.find((r) => r.id === rowId)
367-
if (row) setEditingRow(row)
368-
return
369-
}
370-
371366
if (column.type === 'boolean') {
372367
const row = rowsRef.current.find((r) => r.id === rowId)
373368
if (row) {
@@ -393,6 +388,8 @@ export function Table({
393388
const anchor = selectionAnchorRef.current
394389
if (!anchor || editingCellRef.current || editingEmptyCellRef.current) return
395390

391+
if (!containerRef.current?.contains(e.target as Node)) return
392+
396393
const cols = columnsRef.current
397394
const dataRows = visibleRowsRef.current
398395
const totalRows = dataRows.length + PLACEHOLDER_ROW_COUNT
@@ -558,7 +555,7 @@ export function Table({
558555
if (selectionFocusRef.current !== null) return
559556

560557
const column = columnsRef.current.find((c) => c.name === columnName)
561-
if (!column || column.type === 'json' || column.type === 'boolean') return
558+
if (!column || column.type === 'boolean') return
562559
setEditingEmptyCell({ rowIndex, columnName })
563560
}, [])
564561

@@ -738,7 +735,7 @@ export function Table({
738735
}
739736

740737
return (
741-
<div className='flex h-full flex-col'>
738+
<div ref={containerRef} className='flex h-full flex-col'>
742739
{!embedded && (
743740
<>
744741
<ResourceHeader
@@ -1290,27 +1287,15 @@ function CellContent({
12901287
const isNull = value === null || value === undefined
12911288

12921289
if (column.type === 'boolean') {
1293-
const boolValue = Boolean(value)
1294-
return (
1295-
<span className={boolValue ? 'text-green-500' : 'text-[var(--text-tertiary)]'}>
1296-
{isNull ? '' : boolValue ? 'true' : 'false'}
1297-
</span>
1298-
)
1290+
if (isNull) return null
1291+
return <span className='text-[var(--text-primary)]'>{value ? 'true' : 'false'}</span>
12991292
}
13001293

13011294
if (isNull) return null
13021295

13031296
if (column.type === 'json') {
13041297
return (
1305-
<span className='block truncate font-mono text-[11px] text-[var(--text-secondary)]'>
1306-
{JSON.stringify(value)}
1307-
</span>
1308-
)
1309-
}
1310-
1311-
if (column.type === 'number') {
1312-
return (
1313-
<span className='font-mono text-[12px] text-[var(--text-secondary)]'>{String(value)}</span>
1298+
<span className='block truncate text-[var(--text-primary)]'>{JSON.stringify(value)}</span>
13141299
)
13151300
}
13161301

@@ -1324,7 +1309,7 @@ function CellContent({
13241309
hour: '2-digit',
13251310
minute: '2-digit',
13261311
})
1327-
return <span className='text-[12px] text-[var(--text-secondary)]'>{formatted}</span>
1312+
return <span className='text-[var(--text-primary)]'>{formatted}</span>
13281313
} catch {
13291314
return <span className='text-[var(--text-primary)]'>{String(value)}</span>
13301315
}
@@ -1399,12 +1384,7 @@ function InlineEditor({
13991384
onKeyDown={handleKeyDown}
14001385
onBlur={handleSave}
14011386
className={cn(
1402-
'w-full min-w-0 select-text border-none bg-transparent p-0 outline-none',
1403-
column.type === 'number'
1404-
? 'font-mono text-[12px] text-[var(--text-secondary)]'
1405-
: column.type === 'date'
1406-
? 'text-[12px] text-[var(--text-secondary)]'
1407-
: 'text-[13px] text-[var(--text-primary)]'
1387+
'w-full min-w-0 select-text border-none bg-transparent p-0 text-[13px] text-[var(--text-primary)] outline-none'
14081388
)}
14091389
/>
14101390
)

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function cleanCellValue(value: unknown, column: ColumnDefinition): unknow
4949
export function formatValueForInput(value: unknown, type: string): string {
5050
if (value === null || value === undefined) return ''
5151
if (type === 'json') {
52-
return typeof value === 'string' ? value : JSON.stringify(value, null, 2)
52+
return typeof value === 'string' ? value : JSON.stringify(value)
5353
}
5454
if (type === 'date' && value) {
5555
try {

0 commit comments

Comments
 (0)