Bläddra i källkod

Merge pull request #3556 from deepseek-harness/fix/web-skill-chip-rebuilt-message

fix(web): keep skill chips on rebuilt message nodes
Yichen Jiang 3 veckor sedan
förälder
incheckning
9c13703708

+ 4 - 0
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()

+ 6 - 1
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)
       }

+ 13 - 0
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')])