mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-20 13:53:32 +08:00
fix(desktop): show guided queued prompts immediately (#755)
Insert guided queued prompts into the desktop transcript as soon as the user clicks Guide, then confirm them on CLI replay without adding duplicate user bubbles. Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx -t "queues prompts submitted while a turn is running until the user guides them" Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx Tested: cd desktop && bun run test -- src/stores/chatStore.test.ts Tested: cd desktop && bun run lint Tested: bun run check:desktop Confidence: high Scope-risk: narrow
This commit is contained in:
parent
ea82c6ec70
commit
b70f25e62b
@ -452,22 +452,27 @@ describe('ChatInput file mentions', () => {
|
|||||||
attachments: [],
|
attachments: [],
|
||||||
})
|
})
|
||||||
expect(screen.queryByTestId('pending-user-message')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('pending-user-message')).not.toBeInTheDocument()
|
||||||
expect(useChatStore.getState().sessions[sessionId]?.messages.at(-1)).toMatchObject({
|
expect(useChatStore.getState().sessions[sessionId]?.messages).toMatchObject([
|
||||||
type: 'assistant_text',
|
{ type: 'assistant_text', content: 'workingstill answering' },
|
||||||
content: 'working',
|
{ type: 'user_text', content: 'please adjust the current direction' },
|
||||||
})
|
])
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
useChatStore.getState().handleServerMessage(sessionId, {
|
||||||
|
type: 'tool_result',
|
||||||
|
toolUseId: 'tool-1',
|
||||||
|
content: 'tool finished',
|
||||||
|
isError: false,
|
||||||
|
})
|
||||||
useChatStore.getState().handleServerMessage(sessionId, {
|
useChatStore.getState().handleServerMessage(sessionId, {
|
||||||
type: 'user_message_replay',
|
type: 'user_message_replay',
|
||||||
content: 'please adjust the current direction',
|
content: 'please adjust the current direction',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(useChatStore.getState().sessions[sessionId]?.messages.at(-1)).toMatchObject({
|
const guidedMessages = useChatStore.getState().sessions[sessionId]?.messages
|
||||||
type: 'user_text',
|
.filter((message) => message.type === 'user_text' && message.content === 'please adjust the current direction')
|
||||||
content: 'please adjust the current direction',
|
expect(guidedMessages).toHaveLength(1)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('edits and deletes queued prompts without sending them', async () => {
|
it('edits and deletes queued prompts without sending them', async () => {
|
||||||
|
|||||||
@ -1372,9 +1372,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
const queuedMessage = (session?.queuedUserMessages ?? []).find((message) => message.id === messageId)
|
const queuedMessage = (session?.queuedUserMessages ?? []).find((message) => message.id === messageId)
|
||||||
if (!session || !queuedMessage) return
|
if (!session || !queuedMessage) return
|
||||||
|
|
||||||
get().removeQueuedUserMessage(sessionId, messageId)
|
|
||||||
|
|
||||||
if (session.chatState === 'idle') {
|
if (session.chatState === 'idle') {
|
||||||
|
get().removeQueuedUserMessage(sessionId, messageId)
|
||||||
get().sendMessage(
|
get().sendMessage(
|
||||||
sessionId,
|
sessionId,
|
||||||
queuedMessage.content,
|
queuedMessage.content,
|
||||||
@ -1387,6 +1386,22 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
set((state) => ({
|
||||||
|
sessions: updateSessionIn(state.sessions, sessionId, (currentSession) => {
|
||||||
|
const pendingText = `${currentSession.streamingText}${consumePendingDelta(sessionId)}`
|
||||||
|
const baseMessages = pendingText.trim()
|
||||||
|
? appendAssistantTextMessage(currentSession.messages, pendingText, now)
|
||||||
|
: currentSession.messages
|
||||||
|
return {
|
||||||
|
messages: appendOptimisticQueuedUserMessage(baseMessages, queuedMessage, now),
|
||||||
|
queuedUserMessages: (currentSession.queuedUserMessages ?? [])
|
||||||
|
.filter((message) => message.id !== messageId),
|
||||||
|
...(pendingText.trim() ? { streamingText: '' } : {}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
wsManager.send(sessionId, {
|
wsManager.send(sessionId, {
|
||||||
type: 'user_message',
|
type: 'user_message',
|
||||||
content: queuedMessage.content,
|
content: queuedMessage.content,
|
||||||
@ -2740,6 +2755,19 @@ function appendReplayedUserMessage(
|
|||||||
if (!displayContent) return messages
|
if (!displayContent) return messages
|
||||||
|
|
||||||
const modelContent = parsed.modelContent ?? content.trim()
|
const modelContent = parsed.modelContent ?? content.trim()
|
||||||
|
const optimisticIndex = findOptimisticQueuedUserMessageIndex(messages, modelContent)
|
||||||
|
if (optimisticIndex >= 0) {
|
||||||
|
const optimisticMessage = messages[optimisticIndex]
|
||||||
|
if (optimisticMessage?.type === 'user_text') {
|
||||||
|
const { optimisticQueued: _optimisticQueued, ...confirmedMessage } = optimisticMessage
|
||||||
|
return [
|
||||||
|
...messages.slice(0, optimisticIndex),
|
||||||
|
confirmedMessage,
|
||||||
|
...messages.slice(optimisticIndex + 1),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const last = messages[messages.length - 1]
|
const last = messages[messages.length - 1]
|
||||||
if (
|
if (
|
||||||
last?.type === 'user_text' &&
|
last?.type === 'user_text' &&
|
||||||
@ -2761,6 +2789,63 @@ function appendReplayedUserMessage(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendOptimisticQueuedUserMessage(
|
||||||
|
messages: UIMessage[],
|
||||||
|
message: QueuedUserMessage,
|
||||||
|
timestamp: number,
|
||||||
|
): UIMessage[] {
|
||||||
|
const displayContent = message.displayContent.trim()
|
||||||
|
const modelContent = message.content.trim()
|
||||||
|
const attachments = mapQueuedDisplayAttachments(message.displayAttachments)
|
||||||
|
if (!displayContent && !attachments) return messages
|
||||||
|
|
||||||
|
return [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
id: nextId(),
|
||||||
|
type: 'user_text',
|
||||||
|
content: displayContent,
|
||||||
|
...(modelContent && modelContent !== displayContent ? { modelContent } : {}),
|
||||||
|
...(attachments ? { attachments } : {}),
|
||||||
|
timestamp,
|
||||||
|
optimisticQueued: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapQueuedDisplayAttachments(attachments?: AttachmentRef[]): UIAttachment[] | undefined {
|
||||||
|
if (!attachments?.length) return undefined
|
||||||
|
return attachments.map((attachment) => ({
|
||||||
|
type: attachment.type,
|
||||||
|
name: attachment.name || attachment.path || attachment.mimeType || attachment.type,
|
||||||
|
path: attachment.path,
|
||||||
|
data: attachment.data,
|
||||||
|
mimeType: attachment.mimeType,
|
||||||
|
isDirectory: attachment.isDirectory,
|
||||||
|
lineStart: attachment.lineStart,
|
||||||
|
lineEnd: attachment.lineEnd,
|
||||||
|
note: attachment.note,
|
||||||
|
quote: attachment.quote,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOptimisticQueuedUserMessageIndex(
|
||||||
|
messages: UIMessage[],
|
||||||
|
modelContent: string,
|
||||||
|
): number {
|
||||||
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||||
|
const message = messages[index]
|
||||||
|
if (
|
||||||
|
message?.type === 'user_text' &&
|
||||||
|
message.optimisticQueued &&
|
||||||
|
(message.modelContent ?? message.content).trim() === modelContent
|
||||||
|
) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
function replaceQueuedMessageDisplayContent(
|
function replaceQueuedMessageDisplayContent(
|
||||||
message: QueuedUserMessage,
|
message: QueuedUserMessage,
|
||||||
nextDisplayContent: string,
|
nextDisplayContent: string,
|
||||||
|
|||||||
@ -265,7 +265,7 @@ export type TaskSummaryItem = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UIMessage =
|
export type UIMessage =
|
||||||
| { id: string; type: 'user_text'; content: string; modelContent?: string; transcriptMessageId?: string; timestamp: number; attachments?: UIAttachment[]; pending?: boolean }
|
| { id: string; type: 'user_text'; content: string; modelContent?: string; transcriptMessageId?: string; timestamp: number; attachments?: UIAttachment[]; pending?: boolean; optimisticQueued?: boolean }
|
||||||
| { id: string; type: 'assistant_text'; content: string; transcriptMessageId?: string; timestamp: number; model?: string }
|
| { id: string; type: 'assistant_text'; content: string; transcriptMessageId?: string; timestamp: number; model?: string }
|
||||||
| { id: string; type: 'thinking'; content: string; timestamp: number }
|
| { id: string; type: 'thinking'; content: string; timestamp: number }
|
||||||
| {
|
| {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user