fix(chat): preserve retry attachments and per-answer context snapshots
This commit is contained in:
@@ -71,6 +71,7 @@ export interface Conversation {
|
||||
export interface WorkspaceContext { file_path: string; content: string }
|
||||
|
||||
export interface ChatMessage {
|
||||
context_captured?: boolean
|
||||
attachments?: string[]
|
||||
workspace_context?: WorkspaceContext
|
||||
activity?: Array<{ type: 'thinking'; text: string } | { type: 'tool'; tool_call_id: string }>
|
||||
|
||||
@@ -356,3 +356,38 @@ it('uploads attachments and includes their durable IDs in an attachment-only mes
|
||||
expect(store.pendingAttachments).toEqual([])
|
||||
upload.mockRestore()
|
||||
})
|
||||
|
||||
it.each(['user', 'assistant'] as const)('retries older %s messages with their attachments, preserving pending uploads', async role => {
|
||||
const s=useChatStore(); s.selectedProviderId='real'; s.selectedModel='model'
|
||||
s.pendingAttachments=[{attachment_id:'first.md',name:'first.md'}]
|
||||
await s.sendMessage('first'); vi.mocked(streamChat).mock.calls.at(-1)![1].onDone?.()
|
||||
const old=s.messages[role === 'user' ? 0 : 1]!.message_id
|
||||
s.pendingAttachments=[{attachment_id:'later.md',name:'later.md'}]
|
||||
await s.sendMessage('later'); vi.mocked(streamChat).mock.calls.at(-1)![1].onDone?.()
|
||||
s.pendingAttachments=[{attachment_id:'draft.md',name:'draft.md'}]
|
||||
await s.retryMessage(old, role === 'user' ? 'edited first' : undefined)
|
||||
expect(vi.mocked(streamChat).mock.calls.at(-1)![0].attachments).toEqual(['first.md'])
|
||||
expect(s.pendingAttachments.map(a=>a.attachment_id)).toEqual(['draft.md'])
|
||||
})
|
||||
|
||||
it('restores each answer context after history reload, including explicitly absent workspace context', async () => {
|
||||
const s=useChatStore(); s.selectedProviderId='real'; s.selectedModel='model'
|
||||
const first={file_path:'a.md',content:'A'}; const second={file_path:'b.md',content:'B'}
|
||||
await s.sendMessage('explain',undefined,first); vi.mocked(streamChat).mock.calls.at(-1)![1].onDone?.()
|
||||
const original=s.messages[1]!.message_id
|
||||
await s.retryMessage(original,undefined,second); vi.mocked(streamChat).mock.calls.at(-1)![1].onDone?.()
|
||||
expect(s.messages[0]!.workspace_context).toEqual(first)
|
||||
expect(s.messages[1]!.workspace_context).toEqual(second)
|
||||
vi.mocked(listConversationMessages).mockResolvedValue({items:JSON.parse(JSON.stringify(s.messages)),page:{total:2,limit:500,offset:0}})
|
||||
await s.setActiveConversation(s.activeConversationId!)
|
||||
await s.retryMessage(s.messages[1]!.message_id)
|
||||
expect(vi.mocked(streamChat).mock.calls.at(-1)![0].workspace_context).toEqual(second)
|
||||
vi.mocked(streamChat).mock.calls.at(-1)![1].onDone?.()
|
||||
await s.retryMessage(s.messages[1]!.message_id,undefined,null)
|
||||
vi.mocked(streamChat).mock.calls.at(-1)![1].onDone?.()
|
||||
// API serializes absent captured context as null; do not fall back to the original user snapshot.
|
||||
vi.mocked(listConversationMessages).mockResolvedValue({items:JSON.parse(JSON.stringify(s.messages)),page:{total:2,limit:500,offset:0}})
|
||||
await s.setActiveConversation(s.activeConversationId!)
|
||||
await s.sendMessage('continue')
|
||||
expect(vi.mocked(streamChat).mock.calls.at(-1)![0].workspace_context).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -181,9 +181,15 @@ export const useChatStore = defineStore('chat', () => {
|
||||
async function sendMessage(text: string, retryMessageId?: string, workspaceContext?: WorkspaceContext | null) {
|
||||
const content = text.trim() || (pendingAttachments.value.length ? '请分析附件内容' : '')
|
||||
if (!content || !canSend.value || !selectedProviderId.value || !selectedModel.value) return
|
||||
const context = workspaceContext === undefined ? [...messages.value].reverse().find(m => m.role === 'user')?.workspace_context : workspaceContext
|
||||
const targetIndex = retryMessageId ? messages.value.findIndex(m => m.message_id === retryMessageId) : messages.value.length - 1
|
||||
if (retryMessageId && targetIndex < 0) return
|
||||
const target = messages.value[targetIndex]
|
||||
const source = target?.role === 'assistant' && !target.context_captured
|
||||
? messages.value[targetIndex - 1] : target
|
||||
const context = workspaceContext === undefined ? source?.workspace_context : workspaceContext
|
||||
const snapshot = context ? { ...context } : undefined
|
||||
const attachments = pendingAttachments.value.length ? pendingAttachments.value.map(a=>a.attachment_id) : ([...messages.value].reverse().find(m=>m.role==='user')?.attachments ?? [])
|
||||
const attachments = !retryMessageId && pendingAttachments.value.length
|
||||
? pendingAttachments.value.map(a => a.attachment_id) : [...(source?.attachments ?? [])]
|
||||
const version = ++streamVersion
|
||||
isPreparing.value = true
|
||||
historyError.value = ''
|
||||
@@ -217,6 +223,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const aiMsg = reactive<ChatMessage>({
|
||||
message_id: crypto.randomUUID(), conversation_id: conversationId, role: 'assistant', content: '',
|
||||
created_at: new Date().toISOString(), citations: [], tool_calls: [], activity: [],
|
||||
context_captured: true, workspace_context: snapshot, attachments: [...attachments],
|
||||
})
|
||||
if (retryTarget) {
|
||||
messages.value = messages.value.slice(0, retryIndex)
|
||||
@@ -226,7 +233,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (!regenerate) messages.value.push(userMsg)
|
||||
messages.value.push(aiMsg)
|
||||
inputText.value = ''
|
||||
pendingAttachments.value = []
|
||||
if (!retryMessageId) pendingAttachments.value = []
|
||||
isStreaming.value = true
|
||||
conversation.updated_at = new Date().toISOString()
|
||||
conversation.message_count = messages.value.length
|
||||
@@ -322,7 +329,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const message = messages.value[index]
|
||||
if (!message) return
|
||||
const text = message.role === 'user' ? editedText : messages.value[index - 1]?.content
|
||||
if (text?.trim()) await sendMessage(text, messageId, workspaceContext !== undefined ? workspaceContext : (message.role === 'user' ? message.workspace_context : messages.value[index - 1]?.workspace_context))
|
||||
if (text?.trim()) await sendMessage(text, messageId, workspaceContext)
|
||||
}
|
||||
|
||||
async function switchVersion(messageId: string) {
|
||||
|
||||
Reference in New Issue
Block a user