From be687696c696e8383ef1e2d854a40b120f19a0b2 Mon Sep 17 00:00:00 2001 From: Miya Date: Fri, 5 Jun 2026 11:18:46 +0200 Subject: [PATCH] fix: reconcile after human message sends --- .../hooks/use-message-reconciliation.test.ts | 40 +++++++++++++++++++ .../src/hooks/use-message-reconciliation.ts | 14 +++++++ src/renderer/src/stores/agent-store.ts | 8 +++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/hooks/use-message-reconciliation.test.ts b/src/renderer/src/hooks/use-message-reconciliation.test.ts index 557737e2..56fa6302 100644 --- a/src/renderer/src/hooks/use-message-reconciliation.test.ts +++ b/src/renderer/src/hooks/use-message-reconciliation.test.ts @@ -95,10 +95,27 @@ describe('getActiveMessageReconciliationRequest', () => { } })).toBeNull() }) + + it('schedules reconciliation after a human message send timestamp changes', () => { + const reconciler = { schedule: vi.fn() } + + hooks.scheduleHumanMessageSentReconciliation({ + lastHumanMessageSentAt: 0, + reconciler + }) + hooks.scheduleHumanMessageSentReconciliation({ + lastHumanMessageSentAt: 1_717_000_000_000, + reconciler + }) + + expect(reconciler.schedule).toHaveBeenCalledTimes(1) + expect(reconciler.schedule).toHaveBeenCalledWith('human-message-sent') + }) }) describe('createMessageReconciler', () => { afterEach(() => { + vi.restoreAllMocks() vi.useRealTimers() agentStore.useAgentStore.getState().clearAll() }) @@ -234,4 +251,27 @@ describe('createMessageReconciler', () => { store.reconcileMessages([messagesAfterUpdate[0]]) expect(agentStore.useAgentStore.getState().messages).toBe(messagesAfterUpdate) }) + + it('tracks optimistic human sends for reconciliation triggers', () => { + const now = 1_717_000_123_000 + vi.spyOn(Date, 'now').mockReturnValue(now) + + agentStore.useAgentStore.getState().addHumanMessage('#general', 'hello from human', 'project-1') + + const state = agentStore.useAgentStore.getState() + expect(state.lastHumanMessageSentAt).toBe(now) + expect(state.messages[0]).toMatchObject({ + from: 'human', + to: '#general', + body: 'hello from human', + projectId: 'project-1' + }) + }) + + it('wires human message sends into the reconciliation hook', () => { + const source = hooks.useMessageReconciliation.toString() + expect(source).toContain('s.lastHumanMessageSentAt') + expect(source).toMatch(/scheduleHumanMessageSentReconciliation\([\s\S]*lastHumanMessageSentAt[\s\S]*reconciler/) + expect(source).toMatch(/\[lastHumanMessageSentAt,\s*reconciler\]/) + }) }) diff --git a/src/renderer/src/hooks/use-message-reconciliation.ts b/src/renderer/src/hooks/use-message-reconciliation.ts index a42aa9ed..a136ceca 100644 --- a/src/renderer/src/hooks/use-message-reconciliation.ts +++ b/src/renderer/src/hooks/use-message-reconciliation.ts @@ -58,6 +58,15 @@ export interface MessageReconciler { dispose: () => void } +export function scheduleHumanMessageSentReconciliation(input: { + lastHumanMessageSentAt: number + reconciler: Pick +}): void { + if (input.lastHumanMessageSentAt > 0) { + input.reconciler.schedule('human-message-sent') + } +} + function normalizeChannelName(value: string | undefined): string | null { const normalized = value?.trim().replace(/^#/, '') return normalized || null @@ -206,6 +215,7 @@ export function useMessageReconciliation(): void { const activeTabId = useUIStore((s) => s.activeTabId) const tabs = useUIStore((s) => s.tabs) const brokerStatus = useAgentStore((s) => s.brokerStatus) + const lastHumanMessageSentAt = useAgentStore((s) => s.lastHumanMessageSentAt) const activeTab = getActiveTab(tabs, activeTabId) const activeRoomKey = activeTab?.kind === 'channel' ? `channel:${activeTab.projectId || activeProjectId || ''}:${normalizeChannelName(activeTab.channelName) || ''}` @@ -245,6 +255,10 @@ export function useMessageReconciliation(): void { } }, [brokerStatus, reconciler]) + useEffect(() => { + scheduleHumanMessageSentReconciliation({ lastHumanMessageSentAt, reconciler }) + }, [lastHumanMessageSentAt, reconciler]) + useEffect(() => { const scheduleAfterRefresh = (reason: string): void => { refreshEventStream(reason) diff --git a/src/renderer/src/stores/agent-store.ts b/src/renderer/src/stores/agent-store.ts index c74d3e63..70e68e8b 100644 --- a/src/renderer/src/stores/agent-store.ts +++ b/src/renderer/src/stores/agent-store.ts @@ -417,6 +417,7 @@ interface AgentState { brokerError: string | null brokerErrors: BrokerErrorEntry[] brokerEvents: BrokerEventRecord[] + lastHumanMessageSentAt: number setActiveAgentKey: (key: string | null) => void markAgentActive: (projectId: string | undefined, name: string) => void @@ -460,6 +461,7 @@ export const useAgentStore = create()(subscribeWithSelector((set, ge brokerError: null, brokerErrors: [], brokerEvents: [], + lastHumanMessageSentAt: 0, setActiveAgentKey: (key) => set({ activeAgentKey: key }), @@ -1002,7 +1004,8 @@ export const useAgentStore = create()(subscribeWithSelector((set, ge set((state) => ({ messages: isDuplicateHumanEcho(state.messages, msg) ? state.messages - : capByCount([...state.messages, msg], MAX_CHAT_MESSAGES) + : capByCount([...state.messages, msg], MAX_CHAT_MESSAGES), + lastHumanMessageSentAt: timestamp })) }, @@ -1147,7 +1150,8 @@ export const useAgentStore = create()(subscribeWithSelector((set, ge brokerStatus: 'disconnected', brokerError: null, brokerErrors: [], - brokerEvents: [] + brokerEvents: [], + lastHumanMessageSentAt: 0 }), getAgentBuffer: (projectId, name) => getPtyChunks(getAgentKey(projectId, name))