From 7d09d7d411e94f55ad26329f6ae68b4aa5d0749b Mon Sep 17 00:00:00 2001 From: Octopus Date: Mon, 6 Apr 2026 09:25:55 +0800 Subject: [PATCH 1/2] fix(chat): preserve message history when gateway returns fewer messages during active send During an active send, the history poll (which fires every 4s) can race with a gateway reconnection. If the gateway's view of the session is incomplete at that moment -- because it's mid-reconnect or hasn't fully persisted the conversation yet -- the loaded history contains fewer messages than the local state, causing the entire conversation to vanish from the UI. The user then has to restart ClawX to see their messages again. Add a guard in applyLoadedMessages: if a send is in progress AND the loaded history contains fewer messages than the current local state, keep the local messages instead of replacing them. The next history load after the run completes will reconcile the final state. Fixes #709 --- src/stores/chat.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 0fd0ced83..ffd4589a8 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -1386,6 +1386,23 @@ export const useChatStore = create((set, get) => ({ } } + // Guard: during an active send, don't let a history poll replace the local + // conversation with fewer messages from the gateway. This can happen when + // the gateway is reconnecting after a brief disconnect or hasn't fully + // persisted the conversation yet. Without this guard, the history poll + // causes chat history to vanish mid-conversation (issue #709). + { + const preApplyState = get(); + if ( + preApplyState.sending && + preApplyState.lastUserMessageAt && + finalMessages.length < preApplyState.messages.length && + preApplyState.messages.length > 1 + ) { + finalMessages = preApplyState.messages; + } + } + set({ messages: finalMessages, thinkingLevel, loading: false }); // Extract first user message text as a session label for display in the toolbar. From 691d73c104cdf3156ad1538c716fe70e0dcd59b0 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Wed, 8 Apr 2026 09:25:53 +0800 Subject: [PATCH 2/2] fix(skills): match ClawHub entries by slug when gateway skillKey differs When a skill's gateway skillKey differs from its ClawHub slug, the merge logic failed to find the existing skill, causing it to appear as a duplicate placeholder with 'Recently installed, initializing...' description that never resolves. Fix the matching predicate to also compare against the skill's slug field (which defaults to skillKey when absent from the gateway response), so skills are properly merged regardless of naming differences between the gateway and ClawHub. Fixes #317 --- src/stores/skills.ts | 2 +- tests/unit/skills-errors.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/stores/skills.ts b/src/stores/skills.ts index 5c74ab33b..77daea842 100644 --- a/src/stores/skills.ts +++ b/src/stores/skills.ts @@ -138,7 +138,7 @@ export const useSkillsStore = create((set, get) => ({ // Merge with ClawHub results if (clawhubResult.success && clawhubResult.results) { clawhubResult.results.forEach((cs: ClawHubListResult) => { - const existing = combinedSkills.find(s => s.id === cs.slug); + const existing = combinedSkills.find(s => s.id === cs.slug || s.slug === cs.slug); if (existing) { if (!existing.baseDir && cs.baseDir) { existing.baseDir = cs.baseDir; diff --git a/tests/unit/skills-errors.test.ts b/tests/unit/skills-errors.test.ts index 76769c5c9..6a600c232 100644 --- a/tests/unit/skills-errors.test.ts +++ b/tests/unit/skills-errors.test.ts @@ -15,6 +15,34 @@ vi.mock('@/stores/gateway', () => ({ }, })); +describe('skills store slug matching', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it('matches ClawHub skill to gateway skill when gateway slug differs from skillKey', async () => { + // Gateway returns skillKey "foo-v2" but slug "foo" + rpcMock.mockResolvedValueOnce({ + skills: [{ skillKey: 'foo-v2', slug: 'foo', name: 'Foo Skill', description: 'A skill', disabled: false }], + }); + // ClawHub lists "foo" as installed (matching by slug, not skillKey) + hostApiFetchMock + .mockResolvedValueOnce({ success: true, results: [{ slug: 'foo', version: '1.0.0' }] }) + .mockResolvedValueOnce({}); + + const { useSkillsStore } = await import('@/stores/skills'); + await useSkillsStore.getState().fetchSkills(); + + const skills = useSkillsStore.getState().skills; + // Should be exactly one skill, not two (no placeholder duplicate) + expect(skills).toHaveLength(1); + // The skill should be the gateway skill (not the "Recently installed" placeholder) + expect(skills[0].name).toBe('Foo Skill'); + expect(skills[0].description).not.toBe('Recently installed, initializing...'); + }); +}); + describe('skills store error mapping', () => { beforeEach(() => { vi.resetModules();