Merge pull request #5480 from deepseek-harness/worktree/input-command-concatenation-bug-5ed14e

fix(web): stop claim token decoration from looping on boundary input
This commit is contained in:
Yichen Jiang authored and GitHub committed 2026-10-08 15:01:49 +08:00
commit bb0296a0e5
2 files changed
+33 -2

No files matched your search

@@ -43,9 +43,14 @@ export function registerClaimDecoration(editor: LexicalEditor, activeToken: () =
}
if (text.length > token.length) {
// Typing at the token boundary lands in the styled node; split the
// overflow back out so only the token itself carries the color.
const [tokenNode] = node.splitText(token.length)
// overflow back out so only the token itself carries the color. The
// split copies the token style onto the overflow; it is cleared in this
// pass because Lexical's normalization merges same-style neighbours
// before the overflow's own transform runs, which would rejoin the two
// halves and loop until Lexical aborts the update.
const [tokenNode, overflow] = node.splitText(token.length)
if (tokenNode !== undefined && tokenNode.getStyle() !== TOKEN_STYLE) tokenNode.setStyle(TOKEN_STYLE)
overflow?.setStyle('')
return
}
if (node.getStyle() !== TOKEN_STYLE) node.setStyle(TOKEN_STYLE)
@@ -12,6 +12,7 @@ import './control-row-dom.ts'
import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { $getRoot, $isElementNode, $isTextNode } from 'lexical'
import type { SessionSnapshot } from '@deepseek-ai/dsh-api-session-controller/client'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
import { InputTriggerService } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
@@ -344,6 +345,31 @@ describe('scenario H: backspace breaks the token', () => {
expect(b.shell.snapshot.phase).toBe('plain')
})
it('keeps text typed at the end of the claimed token when the browser inserts it natively', async () => {
const b = await bench()
b.type('/goal')
await vi.waitFor(() => { expect(b.controller.menu.getSnapshot().open).toBe(true) })
fireEvent.keyDown(b.textarea, { key: ' ', keyCode: 32 })
expect(b.shell.snapshot.phase).toBe('claimed')
b.type('/goal 发布')
// A caret at the token/argument boundary sits at the end of the styled
// token node; native insertion writes there and Lexical syncs the DOM
// text with setTextContent plus a caret select.
act(() => {
b.shell.editor.update(() => {
const paragraph = $getRoot().getFirstChild()
const token = $isElementNode(paragraph) ? paragraph.getFirstChild() : null
if (!$isTextNode(token)) throw new Error('missing token node')
token.setTextContent('/goal 立即')
token.select(8, 8)
}, { discrete: true })
})
expect(b.shell.snapshot.draft).toBe('/goal 立即发布')
expect(b.shell.snapshot.phase).toBe('claimed')
act(() => { b.shell.editor.update(() => {}, { discrete: true }) })
expect(b.view.container.querySelector('[data-lexical-text][style*="business-primary"]')?.textContent).toBe('/goal ')
})
it('claim releases automatically; the enter after that goes through adjudication again', async () => {
const b = await bench()
b.type('/goal')