feat: remove "插话" badge displayed in user message
This commit is contained in:
@@ -101,7 +101,7 @@ async function bench() {
|
||||
}
|
||||
const actions = info.props['inputActions'] as {
|
||||
setDraft: (text: string) => void
|
||||
submit: (mode?: 'queue' | 'steer') => void
|
||||
submit: () => void
|
||||
}
|
||||
return { state, actions }
|
||||
}
|
||||
@@ -140,25 +140,25 @@ describe('conversation slot inject surface', () => {
|
||||
const { state, actions } = b.inputSurface(ROOT)
|
||||
// Whitespace-only: the machine treats it as empty — no prompt, draft kept.
|
||||
actions.setDraft(' ')
|
||||
actions.submit('queue')
|
||||
actions.submit()
|
||||
expect(b.sessionFake.prompt).not.toHaveBeenCalled()
|
||||
expect(state.getSnapshot().draft).toBe(' ')
|
||||
// Success: cleared and stays cleared.
|
||||
actions.setDraft('hello')
|
||||
actions.submit('queue')
|
||||
actions.submit()
|
||||
expect(state.getSnapshot().draft).toBe('')
|
||||
await Promise.resolve()
|
||||
expect(b.sessionFake.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'queue')
|
||||
// Failure: restored (draft still empty when the rejection lands).
|
||||
b.sessionFake.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'b', details: { reason: 'b' } } })
|
||||
actions.setDraft('retry me')
|
||||
actions.submit('queue')
|
||||
actions.submit()
|
||||
await vi.waitFor(() => {
|
||||
expect(state.getSnapshot().draft).toBe('retry me')
|
||||
})
|
||||
// Failure landing after new typing: no clobber (restore fills empty only).
|
||||
b.sessionFake.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'b', details: { reason: 'b' } } })
|
||||
actions.submit('queue')
|
||||
actions.submit()
|
||||
actions.setDraft('typed during flight')
|
||||
await new Promise(r => setTimeout(r, 0))
|
||||
expect(state.getSnapshot().draft).toBe('typed during flight')
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('MessageItem arms', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制' }))
|
||||
})
|
||||
|
||||
it('steering bubbles carry the interjection badge and non-text rest blocks, without user actions', () => {
|
||||
it('steering bubbles render text and non-text rest blocks, without user actions or a badge', () => {
|
||||
const view = render(
|
||||
<MessageItem t={t} node={{
|
||||
kind: 'steering', seq: 2, turn: 1, source: null,
|
||||
@@ -107,7 +107,7 @@ describe('MessageItem arms', () => {
|
||||
} as never}
|
||||
/>,
|
||||
)
|
||||
expect(view.getByText('插话')).toBeTruthy()
|
||||
expect(view.queryByText('插话')).toBeNull()
|
||||
expect(view.getByText('steer!')).toBeTruthy()
|
||||
expect(view.getByText(/附加内容块/)).toBeTruthy()
|
||||
expect(view.queryByRole('button', { name: '复制' })).toBeNull()
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('Enter semantics', () => {
|
||||
it('plain Enter submits queue mode through the machine; repeat and empty are suppressed', () => {
|
||||
const { textarea, sink } = bench({ draft: 'hello' })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('hello', 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('hello')
|
||||
fireEvent.keyDown(textarea, { key: 'Enter', repeat: true })
|
||||
expect(sink).toHaveBeenCalledTimes(1)
|
||||
const empty = bench({ draft: ' ' })
|
||||
@@ -195,7 +195,7 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
expect(textarea.disabled).toBe(false) // running no longer locks
|
||||
fireEvent.change(textarea, { target: { value: '排队消息2' } })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('排队消息2', 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('排队消息2')
|
||||
expect(button.getAttribute('aria-label')).toBe('停止生成')
|
||||
fireEvent.click(button)
|
||||
expect(stop).toHaveBeenCalledTimes(1)
|
||||
@@ -211,7 +211,7 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
it('idle primary sends and disables on empty draft', () => {
|
||||
const { button, sink } = bench({ draft: 'go' })
|
||||
fireEvent.click(button)
|
||||
expect(sink).toHaveBeenCalledWith('go', 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('go')
|
||||
const empty = bench()
|
||||
expect(empty.button.disabled).toBe(true)
|
||||
})
|
||||
@@ -320,7 +320,7 @@ describe('machine pending lock', () => {
|
||||
},
|
||||
{ start: 0, end: 6, draftRev: shell.snapshot.draftRev },
|
||||
)
|
||||
shell.submit('queue')
|
||||
shell.submit()
|
||||
})
|
||||
expect(shell.snapshot.phase).toBe('submitting')
|
||||
const textarea = view.container.querySelector('textarea')!
|
||||
|
||||
@@ -40,9 +40,9 @@ function effectAt<T extends InputEffect['type']>(
|
||||
}
|
||||
|
||||
/** Drive plain → adjudicating and hand back the minted attempt. */
|
||||
function enterAdjudicating(m: InputMachine, draft: string, mode: 'queue' | 'steer' = 'queue'): SubmitAttempt {
|
||||
function enterAdjudicating(m: InputMachine, draft: string): SubmitAttempt {
|
||||
m.dispatch({ type: 'draft-changed', draft })
|
||||
const fx = m.dispatch({ type: 'enter', mode })
|
||||
const fx = m.dispatch({ type: 'enter' })
|
||||
return effectAt(fx, 0, 'adjudicate').attempt
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ function enterSubmitting(m: InputMachine, name: string, args: string): { attempt
|
||||
m.dispatch({ type: 'draft-changed', draft: `/${name.slice(0, 2)}` })
|
||||
m.dispatch({ type: 'begin-command', claim, span: spanOf(m, 0, m.state.draft.length) })
|
||||
m.dispatch({ type: 'draft-changed', draft: claim.token + args })
|
||||
const fx = m.dispatch({ type: 'enter', mode: 'queue' })
|
||||
const fx = m.dispatch({ type: 'enter' })
|
||||
return { attempt: effectAt(fx, 0, 'begin-submit').attempt, claim }
|
||||
}
|
||||
|
||||
@@ -63,24 +63,24 @@ function staleAttempt(): SubmitAttempt {
|
||||
describe('input-machine: plain × enter', () => {
|
||||
it('empty and whitespace-only drafts produce nothing', () => {
|
||||
const m = new InputMachine()
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' })).toEqual([])
|
||||
expect(m.dispatch({ type: 'enter' })).toEqual([])
|
||||
m.dispatch({ type: 'draft-changed', draft: ' \n ' })
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' })).toEqual([])
|
||||
expect(m.dispatch({ type: 'enter' })).toEqual([])
|
||||
expect(m.state.phase).toBe('plain')
|
||||
})
|
||||
|
||||
it('non-command text falls to the default sink with the given mode', () => {
|
||||
it('non-command text falls to the default sink', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: 'hello world' })
|
||||
expect(m.dispatch({ type: 'enter', mode: 'steer' }))
|
||||
.toEqual([{ type: 'default-sink', draft: 'hello world', mode: 'steer' }])
|
||||
expect(m.dispatch({ type: 'enter' }))
|
||||
.toEqual([{ type: 'default-sink', draft: 'hello world' }])
|
||||
expect(m.state.phase).toBe('plain')
|
||||
})
|
||||
|
||||
it('leading "/" enters adjudicating with a minted attempt carrying the draft snapshot', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: '/goal x' })
|
||||
const fx = m.dispatch({ type: 'enter', mode: 'queue' })
|
||||
const fx = m.dispatch({ type: 'enter' })
|
||||
const eff = effectAt(fx, 0, 'adjudicate')
|
||||
expect(eff.draft).toBe('/goal x')
|
||||
expect(eff.attempt.draftSnapshot).toBe('/goal x')
|
||||
@@ -91,14 +91,14 @@ describe('input-machine: plain × enter', () => {
|
||||
it('leading is judged after trim including newlines', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: '\n\n/goal x' })
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' })[0]?.type).toBe('adjudicate')
|
||||
expect(m.dispatch({ type: 'enter' })[0]?.type).toBe('adjudicate')
|
||||
})
|
||||
|
||||
it('a non-whitespace prefix before "/" is not leading — default sink', () => {
|
||||
const m = new InputMachine()
|
||||
m.dispatch({ type: 'draft-changed', draft: '第一行\n/goal x' })
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' }))
|
||||
.toEqual([{ type: 'default-sink', draft: '第一行\n/goal x', mode: 'queue' }])
|
||||
expect(m.dispatch({ type: 'enter' }))
|
||||
.toEqual([{ type: 'default-sink', draft: '第一行\n/goal x' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -124,11 +124,11 @@ describe('input-machine: adjudication outcomes', () => {
|
||||
expect(effectAt(b.dispatch({ type: 'adjudicated', attempt: attemptB, outcome: { claim: claimOf('goal') } }), 0, 'begin-submit').args).toBe('x')
|
||||
})
|
||||
|
||||
it('undefined outcome falls back to the default sink preserving the enter mode', () => {
|
||||
it('undefined outcome falls back to the default sink', () => {
|
||||
const m = new InputMachine()
|
||||
const attempt = enterAdjudicating(m, '/unknown thing', 'steer')
|
||||
const attempt = enterAdjudicating(m, '/unknown thing')
|
||||
expect(m.dispatch({ type: 'adjudicated', attempt, outcome: undefined }))
|
||||
.toEqual([{ type: 'default-sink', draft: '/unknown thing', mode: 'steer' }])
|
||||
.toEqual([{ type: 'default-sink', draft: '/unknown thing' }])
|
||||
expect(m.state.phase).toBe('plain')
|
||||
})
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('input-machine: adjudication outcomes', () => {
|
||||
it('enter is a no-op while adjudicating (pending lock)', () => {
|
||||
const m = new InputMachine()
|
||||
enterAdjudicating(m, '/goal x')
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' })).toEqual([])
|
||||
expect(m.dispatch({ type: 'enter' })).toEqual([])
|
||||
expect(m.state.phase).toBe('adjudicating')
|
||||
})
|
||||
|
||||
@@ -587,7 +587,7 @@ describe('input-machine: paste plane', () => {
|
||||
|
||||
const b = new InputMachine()
|
||||
b.dispatch({ type: 'paste-begin', text: 'plain text', selection: { start: 0, end: 0 } })
|
||||
b.dispatch({ type: 'enter', mode: 'queue' })
|
||||
b.dispatch({ type: 'enter' })
|
||||
expect(b.state.paste).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -755,7 +755,7 @@ describe('input-machine: submitting transaction', () => {
|
||||
it('enter and begin-command are locked; draft-changed is recorded without leaving submitting', () => {
|
||||
const m = new InputMachine()
|
||||
enterSubmitting(m, 'goal', 'x')
|
||||
expect(m.dispatch({ type: 'enter', mode: 'queue' })).toEqual([])
|
||||
expect(m.dispatch({ type: 'enter' })).toEqual([])
|
||||
expect(m.dispatch({ type: 'draft-changed', draft: '/goal y' })).toEqual([])
|
||||
expect(m.state).toMatchObject({ phase: 'submitting', draft: '/goal y' })
|
||||
})
|
||||
@@ -768,7 +768,7 @@ describe('input-machine: submitting transaction', () => {
|
||||
m.dispatch({ type: 'draft-changed', draft: '/go', editRange: { start: 0, end: 1, insertedLength: 0 } })
|
||||
m.dispatch({ type: 'begin-command', claim: claimOf('goal'), span: spanOf(m, 0, 3) })
|
||||
m.dispatch({ type: 'draft-changed', draft: '/goal go' })
|
||||
const attempt = effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'begin-submit').attempt
|
||||
const attempt = effectAt(m.dispatch({ type: 'enter' }), 0, 'begin-submit').attempt
|
||||
const fx = m.dispatch({ type: 'submit-settled', attempt, ok: true, outcome: { kind: 'success', text: 'goal set' } })
|
||||
expect(fx).toEqual([{ type: 'notice', level: 'info', text: 'goal set' }])
|
||||
expect(m.state).toMatchObject({ phase: 'plain', draft: '', occurrences: [] })
|
||||
@@ -809,7 +809,7 @@ describe('input-machine: submitting transaction', () => {
|
||||
const m = new InputMachine()
|
||||
const { attempt: first } = enterSubmitting(m, 'goal', 'x')
|
||||
m.dispatch({ type: 'submit-settled', attempt: first, ok: false, message: 'retry' })
|
||||
const second = effectAt(m.dispatch({ type: 'enter', mode: 'queue' }), 0, 'begin-submit').attempt
|
||||
const second = effectAt(m.dispatch({ type: 'enter' }), 0, 'begin-submit').attempt
|
||||
expect(second.seq).not.toBe(first.seq)
|
||||
expect(m.dispatch({ type: 'submit-settled', attempt: first, ok: true })).toEqual([])
|
||||
expect(m.state.phase).toBe('submitting')
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('matrix row: plain', () => {
|
||||
fireEvent.change(textarea, { target: { value: '普通消息' } })
|
||||
expect(shell.snapshot.claim).toBeUndefined()
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('普通消息', 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('普通消息')
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
})
|
||||
})
|
||||
@@ -184,7 +184,7 @@ describe('matrix row: locked (session disabled)', () => {
|
||||
expect((textarea).disabled).toBe(false)
|
||||
fireEvent.change(textarea, { target: { value: '排队' } })
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
expect(sink).toHaveBeenCalledWith('排队', 'queue')
|
||||
expect(sink).toHaveBeenCalledWith('排队')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ describe('scenario D: execute-kind /compact', () => {
|
||||
act(() => { b2.shell.setDraft('/compact 现在') })
|
||||
fireEvent.keyDown(b2.textarea, { key: 'Enter' })
|
||||
// execute with trailing → matchEnter answers undefined → default sink.
|
||||
await vi.waitFor(() => { expect(b2.sink).toHaveBeenCalledWith('/compact 现在', 'queue') })
|
||||
await vi.waitFor(() => { expect(b2.sink).toHaveBeenCalledWith('/compact 现在') })
|
||||
expect(b2.executed).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -278,7 +278,7 @@ describe('scenario I: unknown /xyz + enter', () => {
|
||||
const b = await bench()
|
||||
act(() => { b.shell.setDraft('/xyz 干点啥') })
|
||||
fireEvent.keyDown(b.textarea, { key: 'Enter' })
|
||||
await vi.waitFor(() => { expect(b.sink).toHaveBeenCalledWith('/xyz 干点啥', 'queue') })
|
||||
await vi.waitFor(() => { expect(b.sink).toHaveBeenCalledWith('/xyz 干点啥') })
|
||||
expect(b.shell.snapshot.phase).toBe('plain')
|
||||
expect(b.execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -33,11 +33,11 @@ async function bench() {
|
||||
describe('ConversationService', () => {
|
||||
it('routes operations through the public Session binding', async () => {
|
||||
const b = await bench()
|
||||
await b.scoped.send('hello', 'steer')
|
||||
await b.scoped.send('hello')
|
||||
await b.scoped.updateQueue('item-1' as never, { kind: 'remove' })
|
||||
await b.scoped.cancel()
|
||||
await b.scoped.loadOlder()
|
||||
expect(b.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'steer')
|
||||
expect(b.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'queue')
|
||||
expect(b.updateQueue).toHaveBeenCalledWith('item-1', { kind: 'remove' })
|
||||
expect(b.cancel).toHaveBeenCalledOnce()
|
||||
expect(b.loadOlder).toHaveBeenCalledOnce()
|
||||
@@ -47,7 +47,7 @@ describe('ConversationService', () => {
|
||||
it('folds Session business failures into callback rejections', async () => {
|
||||
const b = await bench()
|
||||
b.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'busy', details: {} } } as never)
|
||||
await expect(b.scoped.send('x', 'queue')).rejects.toThrow('conversation.send failed: agent-busy: busy')
|
||||
await expect(b.scoped.send('x')).rejects.toThrow('conversation.send failed: agent-busy: busy')
|
||||
b.cancel.mockResolvedValueOnce({ ok: false, error: { code: 'internal', message: 'nope', details: {} } } as never)
|
||||
await expect(b.scoped.cancel()).rejects.toThrow('conversation.cancel failed: internal: nope')
|
||||
await b.runtime.dispose()
|
||||
@@ -55,9 +55,9 @@ describe('ConversationService', () => {
|
||||
|
||||
it('fails loudly from the root scope, on an unbound session, or without SessionsService', async () => {
|
||||
const b = await bench()
|
||||
await expect(b.root.send('x', 'queue')).rejects.toThrow(/requires a session scope/)
|
||||
await expect(b.root.send('x')).rejects.toThrow(/requires a session scope/)
|
||||
await b.runtime.sessions.remove('s1')
|
||||
await expect(b.scoped.send('x', 'queue')).rejects.toThrow(/resolved no binding/)
|
||||
await expect(b.scoped.send('x')).rejects.toThrow(/resolved no binding/)
|
||||
await b.runtime.dispose()
|
||||
// No SessionsService at all: a bare context (the runtime always provides one).
|
||||
const bare = new Context()
|
||||
@@ -65,6 +65,6 @@ describe('ConversationService', () => {
|
||||
input: new InputHub(bare),
|
||||
}).await()
|
||||
const orphan = bare.get('conversation') as ConversationService
|
||||
await expect(orphan.send('x', 'queue')).rejects.toThrow(/sessions service unavailable/)
|
||||
await expect(orphan.send('x')).rejects.toThrow(/sessions service unavailable/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
fireEvent.change(box, { target: { value: 'ordinary revised' } })
|
||||
expect(b.chat.store.getSnapshot().draft).toBe('ordinary revised')
|
||||
fireEvent.keyDown(box, { key: 'Enter' })
|
||||
expect(b.sink).toHaveBeenCalledWith('ordinary revised', 'queue')
|
||||
expect(b.sink).toHaveBeenCalledWith('ordinary revised')
|
||||
fireEvent.click(b.view.getByRole('button', { name: 'Root' }))
|
||||
expect(b.open).toHaveBeenCalledWith(sid('root'))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user