feat(web): add session model selector

This commit is contained in:
Yichen Jiang
2026-07-24 14:55:54 +08:00
parent bc7a89b81f
commit 208a44a7ec
87 changed files with 2236 additions and 87 deletions

View File

@@ -29,6 +29,7 @@ function snapshotBase(): ConversationSnapshot {
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
pending: [], running: false, removed: false, openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
modelSelection: { current: null, groups: [], failures: [], status: 'idle', error: null },
}
}

View File

@@ -41,7 +41,8 @@ function snapshotWith(nodes: ToolResultNode[]): ConversationSnapshot {
sessionId: SID, nodes, foldDegraded: false, partial: null, runningCalls: [],
pending: [], running: false, removed: false, openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
} as ConversationSnapshot
modelSelection: { current: null, groups: [], failures: [], status: 'idle', error: null },
}
}
/** Test-owned AppFrame role: declares the layout-owned children and renders the conversation area under the framework session provider. */

View File

@@ -31,6 +31,7 @@ function snapshotBase(): ConversationSnapshot {
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
pending: [], running: false, removed: false, openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
modelSelection: { current: null, groups: [], failures: [], status: 'idle', error: null },
}
}

View File

@@ -26,7 +26,8 @@ function snapshotBase(): ConversationSnapshot {
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
pending: [], running: false, removed: false, openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
} as ConversationSnapshot
modelSelection: { current: null, groups: [], failures: [], status: 'idle', error: null },
}
}
describe('render branch tails', () => {

View File

@@ -128,4 +128,11 @@ describe('error strip and variants', () => {
expect(view.getByTestId('acc')).toBeTruthy()
expect(view.container.querySelector('[class*="hero"]')).not.toBeNull()
})
it('renders the optional composer control immediately before the primary button', () => {
const { view } = setup({ control: <button type="button">model</button> })
const buttons = view.getAllByRole('button')
expect(buttons.map(button => button.textContent)).toEqual(['model', ''])
expect(buttons[1]?.getAttribute('aria-label')).toBe('发送')
})
})

View File

@@ -30,7 +30,8 @@ function snapshotBase(): ConversationSnapshot {
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
pending: [], running: false, removed: false, openState: 'open', openError: null,
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
} as ConversationSnapshot
modelSelection: { current: null, groups: [], failures: [], status: 'idle', error: null },
}
}
function sessionSource(over?: Partial<ConversationSnapshot>) {
@@ -58,7 +59,9 @@ function listHook(rows: { id: string; title: string; cwd?: string; parentId?: st
describe('ConversationRoot branches', () => {
const chatTab: ViewTab = { id: 'chat', label: 'Chat' }
/** renderSlot stub in the outlet's baked shape (ring key + only filter marker). */
const stubRenderSlot = (() => <div data-testid="view-body" />) as unknown as ConversationRootProps['renderSlot']
const stubRenderSlot = ((key: string) =>
key === 'conversation.view' ? <div data-testid="view-body" /> : null
) as unknown as ConversationRootProps['renderSlot']
/** SessionProvider seat stub (render-prop pass-through; ConversationRoot never invokes it). */
const SessionProviderStub: ConversationRootProps['SessionProvider'] = ({ children }) => <>{children(SID)}</>

View File

@@ -177,8 +177,11 @@ describe('ConversationRoot', () => {
})
it('hides the tab strip with a single view; composer writes the store draft and sends it', () => {
const { chat, send } = bench([tab('chat', 'Chat')])
const { chat, send, renderSlot } = bench([tab('chat', 'Chat')])
expect(screen.queryByRole('tablist')).toBeNull()
expect(renderSlot).toHaveBeenCalledWith('conversation.composer.control', {})
expect(screen.getByTestId('view-(all)').getAttribute('data-slot'))
.toBe('conversation.composer.control')
const box = screen.getByPlaceholderText(/输入消息/)
fireEvent.change(box, { target: { value: 'hi' } })
// Typing goes through actions.setDraft into the shared store.