Merge latest master into status bar token metrics
This commit is contained in:
@@ -188,9 +188,11 @@ describe('conversation slot inject surface', () => {
|
||||
// hooks compartment still present so the render side's hook order holds.
|
||||
const absent = injectFn(undefined)
|
||||
expect(absent.keyboard).toBeUndefined()
|
||||
expect(absent.toggleCommandMenu).toBeUndefined()
|
||||
expect(absent.stop).toBeUndefined()
|
||||
expect(absent.hooks.notices.getSnapshot()).toBeNull()
|
||||
expect(absent.hooks.lexicon.getSnapshot().size).toBe(0)
|
||||
expect(absent.hooks.menuLauncher.getSnapshot()).toBeNull()
|
||||
// A scope whose service tree lost 'conversation' (the feature fiber
|
||||
// unloaded while a retained inject closure re-runs): fails loud too.
|
||||
const stop = injectFn(ROOT).stop!
|
||||
|
||||
@@ -50,6 +50,8 @@ interface BenchOptions {
|
||||
overlay?: React.ReactNode
|
||||
leftItems?: React.ReactNode
|
||||
rightItems?: React.ReactNode
|
||||
commandMenuOpen?: boolean
|
||||
toggleCommandMenu?: (selection: { start: number; end: number }) => void
|
||||
}
|
||||
|
||||
/** Real machine behind the bar entry: sink spy, no slash pipeline (plain text goes straight to the sink). */
|
||||
@@ -77,6 +79,7 @@ function bench(over?: BenchOptions) {
|
||||
promptError: over?.promptError ?? null,
|
||||
}))
|
||||
const stop = vi.fn()
|
||||
const menuLauncher = createSnapshotStore<string | null>(over?.commandMenuOpen === true ? 'command' : null)
|
||||
const slotCalls: { key: string; owner: unknown }[] = []
|
||||
const renderSlot = ((key: string, owner: object) => {
|
||||
slotCalls.push({ key, owner })
|
||||
@@ -100,8 +103,10 @@ function bench(over?: BenchOptions) {
|
||||
useInput: bindSnapshotSelector(shell.state),
|
||||
inputActions: shell.actions,
|
||||
keyboard: shell,
|
||||
toggleCommandMenu: over?.toggleCommandMenu ?? vi.fn(),
|
||||
useNotices: bindSnapshotSelector(shell.notices),
|
||||
useLexicon: bindSnapshotSelector(shell.lexicon),
|
||||
useMenuLauncher: bindSnapshotSelector(menuLauncher),
|
||||
stop,
|
||||
command: () => Promise.resolve(true),
|
||||
// Mirrors the real lookup chain (conversation namespace, then common).
|
||||
@@ -120,7 +125,7 @@ function bench(over?: BenchOptions) {
|
||||
const button = view.container.querySelector<HTMLButtonElement>(
|
||||
`button[aria-label="${over?.running === true ? '停止生成' : '发送消息'}"]`,
|
||||
)!
|
||||
return { view, textarea, button, props, sink, shell, wiring: shell, session, stop, slotCalls }
|
||||
return { view, textarea, button, props, sink, shell, wiring: shell, session, stop, slotCalls, menuLauncher }
|
||||
}
|
||||
|
||||
describe('Enter semantics', () => {
|
||||
@@ -205,7 +210,7 @@ describe('running and lock semantics (queue cut 1)', () => {
|
||||
const { textarea, view } = bench({ disabled: true })
|
||||
expect(textarea.disabled).toBe(true)
|
||||
expect(textarea.placeholder).toBe('会话不可用')
|
||||
expect((view.getByLabelText('添加附件') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText('命令') as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('idle primary sends and disables on empty draft', () => {
|
||||
@@ -438,10 +443,10 @@ describe('strips and variants', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('placeholder chrome and control seats', () => {
|
||||
it('renders attach; the Access chip is absent without the permissions projection; plan/model seats render EMPTY without entries (B ruling)', () => {
|
||||
describe('command launcher chrome and control seats', () => {
|
||||
it('renders the command launcher; the Access chip is absent without the permissions projection; plan/model seats render EMPTY without entries (B ruling)', () => {
|
||||
const { view, slotCalls } = bench()
|
||||
expect(view.getByLabelText('添加附件')).toBeTruthy()
|
||||
expect(view.getByLabelText('命令')).toBeTruthy()
|
||||
// Capability absent (no projection value): the chip renders nothing.
|
||||
expect(view.queryByLabelText(/^访问模式/)).toBeNull()
|
||||
// Both seats dispatched, nothing rendered.
|
||||
@@ -450,6 +455,18 @@ describe('placeholder chrome and control seats', () => {
|
||||
expect(view.queryByLabelText('Model')).toBeNull()
|
||||
})
|
||||
|
||||
it('passes the textarea selection to the command menu launcher and reflects its expanded state', () => {
|
||||
const toggleCommandMenu = vi.fn()
|
||||
const { view, textarea, menuLauncher } = bench({ draft: 'draft text', toggleCommandMenu })
|
||||
textarea.setSelectionRange(2, 7)
|
||||
const launcher = view.getByLabelText('命令')
|
||||
expect(launcher.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.click(launcher)
|
||||
expect(toggleCommandMenu).toHaveBeenCalledExactlyOnceWith({ start: 2, end: 7 })
|
||||
act(() => { menuLauncher.set('command') })
|
||||
expect(launcher.getAttribute('aria-expanded')).toBe('true')
|
||||
})
|
||||
|
||||
it('the Access chip renders the projection value and submits /permission on pick', async () => {
|
||||
const permissions = {
|
||||
options: [
|
||||
@@ -489,10 +506,10 @@ describe('placeholder chrome and control seats', () => {
|
||||
expect(live.slotCalls.every(c => !(c.owner as { locked: boolean }).locked)).toBe(true)
|
||||
})
|
||||
|
||||
it('disabled locks the Access chip and attach control (running does not)', () => {
|
||||
it('disabled locks the Access chip and command launcher (running does not)', () => {
|
||||
const permissions = { options: [{ value: 'workspace-write', name: 'workspace-write' }], currentValue: 'workspace-write' }
|
||||
const { view } = bench({ disabled: true, permissions })
|
||||
expect((view.getByLabelText('添加附件') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText('命令') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText(/^访问模式/) as HTMLButtonElement).disabled).toBe(true)
|
||||
cleanup()
|
||||
const live = bench({ running: true, permissions })
|
||||
|
||||
@@ -46,8 +46,10 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled
|
||||
useInput: bindSnapshotSelector(shell.state),
|
||||
inputActions: shell.actions,
|
||||
keyboard: shell,
|
||||
toggleCommandMenu: vi.fn(),
|
||||
useNotices: bindSnapshotSelector(shell.notices),
|
||||
useLexicon: bindSnapshotSelector(shell.lexicon),
|
||||
useMenuLauncher: bindSnapshotSelector(createSnapshotStore<string | null>(null)),
|
||||
renderSlot: (() => null) as InputBarProps['renderSlot'],
|
||||
stop: vi.fn(),
|
||||
command: () => Promise.resolve(true),
|
||||
@@ -175,7 +177,7 @@ describe('matrix row: locked (session disabled)', () => {
|
||||
it('disables the textarea and chrome; the machine currency is untouched', () => {
|
||||
const { view, textarea, shell } = bench({ disabled: true })
|
||||
expect((textarea).disabled).toBe(true)
|
||||
expect((view.getByLabelText('添加附件') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect((view.getByLabelText('命令') as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(shell.snapshot.phase).toBe('plain')
|
||||
})
|
||||
|
||||
|
||||
@@ -132,8 +132,18 @@ async function scopedBench(register?: (slash: SlashService) => void) {
|
||||
useInput: bindSnapshotSelector(shell.state),
|
||||
inputActions: shell.actions,
|
||||
keyboard: shell,
|
||||
toggleCommandMenu: (selection) => {
|
||||
const snapshot = shell.snapshot
|
||||
controller.toggleSource('command', {
|
||||
trigger: '/',
|
||||
query: '',
|
||||
position: snapshot.draft.slice(0, selection.start).trim() === '' ? 'leading' : 'inline',
|
||||
span: { ...selection, draftRev: snapshot.draftRev },
|
||||
})
|
||||
},
|
||||
useNotices: bindSnapshotSelector(shell.notices),
|
||||
useLexicon: bindSnapshotSelector(shell.lexicon),
|
||||
useMenuLauncher: bindSnapshotSelector(controller.launcher),
|
||||
renderSlot: (() => null) as InputBarProps['renderSlot'],
|
||||
stop: vi.fn(),
|
||||
command: () => Promise.resolve(true),
|
||||
|
||||
@@ -152,8 +152,10 @@ function mount(
|
||||
useInput={useInput}
|
||||
inputActions={inputActions}
|
||||
keyboard={wiring}
|
||||
toggleCommandMenu={vi.fn()}
|
||||
useNotices={bindSnapshotSelector(wiring.notices)}
|
||||
useLexicon={bindSnapshotSelector(wiring.lexicon)}
|
||||
useMenuLauncher={bindSnapshotSelector(createSnapshotStore<string | null>(null))}
|
||||
stop={stop}
|
||||
command={() => Promise.resolve(true)}
|
||||
t={t}
|
||||
|
||||
Reference in New Issue
Block a user