diff --git a/apps/web/tests/skill-user-invoke.e2e.ts b/apps/web/tests/skill-user-invoke.e2e.ts index e9b6891b7b..5201f23a96 100644 --- a/apps/web/tests/skill-user-invoke.e2e.ts +++ b/apps/web/tests/skill-user-invoke.e2e.ts @@ -130,6 +130,10 @@ describe.skipIf(MODE === 'record')('web e2e: user-explicit skill invocation thro await settled const process = page.getByRole('button', { name: 'Thought for a while', exact: true }) await process.waitFor({ state: 'visible', timeout: 10_000 }) + // The chip derives from the step's logged injection, so it must survive + // every later Node rebuild of the Turn (process publication, turn close). + expect(await bubble.count()).toBe(1) + expect(await bubble.textContent()).toBe(`/${SKILL_NAME}`) await expandOwningTurnProcess(page, injectionFlow) const injectionRow = page.getByRole('button', { name: `Context injection ${SKILL_NAME}` }) await injectionRow.click() diff --git a/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts b/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts index 34f544c4ff..04cb38bc94 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts +++ b/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts @@ -616,7 +616,12 @@ export class SkillNameProjector { const next = slashEntryOf(node) const previous = this.entries.get(node.key) if (previous !== undefined) { - if (next !== null && sameSlashEntry(previous, next)) continue + if (next !== null && sameSlashEntry(previous, next)) { + // A rebuilt message Node carries Definition state only, never the + // names a previous apply attached: re-read its batch. + if (next.kind === 'message') dirty.push(next.seq) + continue + } this.remove(previous) dirty.push(previous.seq) } diff --git a/packages/client/ui-chat/tests/skill-name-projector.client.spec.ts b/packages/client/ui-chat/tests/skill-name-projector.client.spec.ts index 0be8dca394..6e1f375adb 100644 --- a/packages/client/ui-chat/tests/skill-name-projector.client.spec.ts +++ b/packages/client/ui-chat/tests/skill-name-projector.client.spec.ts @@ -76,6 +76,19 @@ describe('SkillNameProjector', () => { expect(names(out.find(node => node.key === 'user:2'))).toEqual(['demo']) }) + it('re-decorates a direct message the assembler re-emits without names', () => { + const projector = new SkillNameProjector() + const replaced = projector.replace([user(2, '/demo go'), skill(5, 'demo'), assistant(6)]) + expect(names(replaced[0])).toEqual(['demo']) + const store = storeOf(replaced) + // A rebuilt message Node carries Definition state only: no skillNames. + const rebuilt = user(2, '/demo go') + const out = projector.apply([rebuilt], store) + expect(out.map(node => node.key)).toEqual(['user:2']) + expect(names(out[0])).toEqual(['demo']) + expect(store.valuesCalls).toBe(0) + }) + it('a boundary arriving inside a batch splits it and drops the names past it', () => { const projector = new SkillNameProjector() const replaced = projector.replace([user(2, '/demo go'), skill(4, 'demo'), user(8, '/demo again')])