diff --git a/cc-haha/desktop/package.json b/cc-haha/desktop/package.json index 4d1f749..b7514b0 100644 --- a/cc-haha/desktop/package.json +++ b/cc-haha/desktop/package.json @@ -1,7 +1,7 @@ { "name": "heicode-desktop", "private": true, - "version": "0.1.7", + "version": "0.1.8", "type": "module", "scripts": { "dev": "vite", diff --git a/cc-haha/desktop/src-tauri/Cargo.toml b/cc-haha/desktop/src-tauri/Cargo.toml index 75da4b0..58d5b43 100644 --- a/cc-haha/desktop/src-tauri/Cargo.toml +++ b/cc-haha/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "heicode-desktop" -version = "0.1.7" +version = "0.1.8" edition = "2021" [lib] diff --git a/cc-haha/desktop/src-tauri/tauri.conf.json b/cc-haha/desktop/src-tauri/tauri.conf.json index ba804fb..b31c348 100644 --- a/cc-haha/desktop/src-tauri/tauri.conf.json +++ b/cc-haha/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json", "productName": "HeiCode", - "version": "0.1.7", + "version": "0.1.8", "identifier": "com.heicode.desktop", "build": { "frontendDist": "../dist", diff --git a/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx b/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx index bece547..03bb52a 100644 --- a/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx +++ b/cc-haha/desktop/src/components/chat/AskUserQuestion.tsx @@ -76,8 +76,9 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) { const [hasSubmitted, setHasSubmitted] = useState(false) const composingRef = useRef(false) - if (questions.length === 0) return null - + // All `useMemo`/`useEffect`-like calls must stay above any early return, + // otherwise React's hook-ordering invariant breaks the next render. The + // empty-questions short-circuit happens *after* all hooks are registered. const resultAnswers = useMemo(() => { if (!result || typeof result !== 'object') return {} const answers = (result as { answers?: unknown }).answers @@ -98,6 +99,8 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) { }, [freeText, questions, resultAnswers, selections]) const submitted = Object.keys(resultAnswers).length > 0 || hasSubmitted + if (questions.length === 0) return null + const handleSelect = (qIndex: number, label: string) => { if (submitted) return const isMulti = !!questions[qIndex]?.multiSelect diff --git a/cc-haha/desktop/src/stores/chatStore.ts b/cc-haha/desktop/src/stores/chatStore.ts index 0170100..be2b04b 100644 --- a/cc-haha/desktop/src/stores/chatStore.ts +++ b/cc-haha/desktop/src/stores/chatStore.ts @@ -127,20 +127,44 @@ const pendingTaskToolUseIds = new Set() let msgCounter = 0 const nextId = () => `msg-${++msgCounter}-${Date.now()}` -// Streaming throttle for content_delta -let pendingDelta = '' -let flushTimer: ReturnType | null = null +// Streaming throttle for content_delta — **per session**. The previous +// implementation kept `pendingDelta` and `flushTimer` at module scope, +// which meant two sessions streaming at the same time would interleave +// their text into whichever session's flush callback fired first. +// Keyed by sessionId so each in-flight stream owns its own buffer. +type DeltaBuffer = { + pending: string + timer: ReturnType | null +} +const deltaBuffers = new Map() -function consumePendingDelta(): string { - if (flushTimer) { - clearTimeout(flushTimer) - flushTimer = null +function getBuffer(sessionId: string): DeltaBuffer { + let b = deltaBuffers.get(sessionId) + if (!b) { + b = { pending: '', timer: null } + deltaBuffers.set(sessionId, b) } - const text = pendingDelta - pendingDelta = '' + return b +} + +function consumePendingDelta(sessionId: string): string { + const b = deltaBuffers.get(sessionId) + if (!b) return '' + if (b.timer) { + clearTimeout(b.timer) + b.timer = null + } + const text = b.pending + b.pending = '' return text } +function dropBuffer(sessionId: string): void { + const b = deltaBuffers.get(sessionId) + if (b?.timer) clearTimeout(b.timer) + deltaBuffers.delete(sessionId) +} + function appendAssistantTextMessage( messages: UIMessage[], content: string, @@ -249,11 +273,11 @@ export const useChatStore = create((set, get) => ({ disconnectSession: (sessionId) => { const session = get().sessions[sessionId] if (session?.elapsedTimer) clearInterval(session.elapsedTimer) - if (flushTimer) { clearTimeout(flushTimer); flushTimer = null } - if (pendingDelta) { - const text = consumePendingDelta() - set((s) => ({ sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({ streamingText: sess.streamingText + text })) })) + const tail = consumePendingDelta(sessionId) + if (tail) { + set((s) => ({ sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({ streamingText: sess.streamingText + tail })) })) } + dropBuffer(sessionId) wsManager.disconnect(sessionId) set((s) => { const { [sessionId]: _, ...rest } = s.sessions @@ -287,11 +311,7 @@ export const useChatStore = create((set, get) => ({ set((s) => { const session = s.sessions[sessionId] ?? createDefaultSessionState() - if (flushTimer) { - clearTimeout(flushTimer) - flushTimer = null - } - const bufferedDelta = consumePendingDelta() + const bufferedDelta = consumePendingDelta(sessionId) const pendingAssistantText = `${session.streamingText}${bufferedDelta}` const newMessages = pendingAssistantText.trim() @@ -403,10 +423,9 @@ export const useChatStore = create((set, get) => ({ stopGeneration: (sessionId) => { wsManager.send(sessionId, { type: 'stop_generation' }) - if (flushTimer) { clearTimeout(flushTimer); flushTimer = null } - if (pendingDelta) { - const text = consumePendingDelta() - set((s) => ({ sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({ streamingText: sess.streamingText + text })) })) + const stopTail = consumePendingDelta(sessionId) + if (stopTail) { + set((s) => ({ sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({ streamingText: sess.streamingText + stopTail })) })) } set((s) => { const session = s.sessions[sessionId] @@ -528,7 +547,7 @@ export const useChatStore = create((set, get) => ({ case 'status': update((session) => { - const pendingText = `${session.streamingText}${consumePendingDelta()}` + const pendingText = `${session.streamingText}${consumePendingDelta(sessionId)}` const hasPendingStreamText = session.chatState === 'streaming' && pendingText.trim().length > 0 // Background task progress can arrive while the assistant is still @@ -562,7 +581,7 @@ export const useChatStore = create((set, get) => ({ case 'content_start': { const session = get().sessions[sessionId] if (!session) break - const pendingText = `${session.streamingText}${consumePendingDelta()}` + const pendingText = `${session.streamingText}${consumePendingDelta(sessionId)}` if (msg.blockType !== 'text' && pendingText.trim()) { update((s) => ({ messages: appendAssistantTextMessage(s.messages, pendingText, Date.now()), @@ -589,12 +608,13 @@ export const useChatStore = create((set, get) => ({ case 'content_delta': if (msg.text !== undefined) { - pendingDelta += msg.text - if (!flushTimer) { - flushTimer = setTimeout(() => { - const text = pendingDelta - pendingDelta = '' - flushTimer = null + const buf = getBuffer(sessionId) + buf.pending += msg.text + if (!buf.timer) { + buf.timer = setTimeout(() => { + const text = buf.pending + buf.pending = '' + buf.timer = null update((s) => ({ streamingText: s.streamingText + text })) }, 50) } @@ -604,7 +624,7 @@ export const useChatStore = create((set, get) => ({ case 'thinking': update((s) => { - const pendingText = `${s.streamingText}${consumePendingDelta()}` + const pendingText = `${s.streamingText}${consumePendingDelta(sessionId)}` const base = pendingText.trim() ? appendAssistantTextMessage(s.messages, pendingText, Date.now()) : s.messages @@ -701,7 +721,7 @@ export const useChatStore = create((set, get) => ({ case 'message_complete': { const session = get().sessions[sessionId] if (!session) break - const text = `${session.streamingText}${consumePendingDelta()}` + const text = `${session.streamingText}${consumePendingDelta(sessionId)}` if (text.trim()) { update((s) => ({ messages: appendAssistantTextMessage(s.messages, text, Date.now()), @@ -724,7 +744,7 @@ export const useChatStore = create((set, get) => ({ case 'error': update((s) => { - const pendingText = `${s.streamingText}${consumePendingDelta()}` + const pendingText = `${s.streamingText}${consumePendingDelta(sessionId)}` let newMessages = s.messages if (pendingText.trim()) { newMessages = appendAssistantTextMessage(newMessages, pendingText, Date.now())