@@ -1387,6 +1387,18 @@ function dispatchChatNewShortcut(): void {
13871387 ) ;
13881388}
13891389
1390+ function releaseModShortcut ( key ?: string ) : void {
1391+ window . dispatchEvent (
1392+ new KeyboardEvent ( "keyup" , {
1393+ key : key ?? ( isMacPlatform ( navigator . platform ) ? "Meta" : "Control" ) ,
1394+ metaKey : false ,
1395+ ctrlKey : false ,
1396+ bubbles : true ,
1397+ cancelable : true ,
1398+ } ) ,
1399+ ) ;
1400+ }
1401+
13901402async function triggerChatNewShortcutUntilPath (
13911403 router : ReturnType < typeof getRouter > ,
13921404 predicate : ( pathname : string ) => boolean ,
@@ -3663,6 +3675,29 @@ describe("ChatView timeline estimator parity (full app)", () => {
36633675 node : { type : "identifier" , name : "terminalFocus" } ,
36643676 } ,
36653677 } ,
3678+ {
3679+ command : "thread.jump.1" ,
3680+ shortcut : {
3681+ key : "1" ,
3682+ metaKey : true ,
3683+ ctrlKey : false ,
3684+ shiftKey : false ,
3685+ altKey : false ,
3686+ modKey : false ,
3687+ } ,
3688+ } ,
3689+ {
3690+ command : "modelPicker.jump.1" ,
3691+ shortcut : {
3692+ key : "1" ,
3693+ metaKey : true ,
3694+ ctrlKey : false ,
3695+ shiftKey : false ,
3696+ altKey : false ,
3697+ modKey : false ,
3698+ } ,
3699+ whenAst : { type : "identifier" , name : "modelPickerOpen" } ,
3700+ } ,
36663701 ] ,
36673702 } ;
36683703 } ,
@@ -4477,6 +4512,208 @@ describe("ChatView timeline estimator parity (full app)", () => {
44774512 }
44784513 } ) ;
44794514
4515+ it ( "opens the model picker when selecting /model" , async ( ) => {
4516+ const mounted = await mountChatView ( {
4517+ viewport : DEFAULT_VIEWPORT ,
4518+ snapshot : createSnapshotForTargetUser ( {
4519+ targetMessageId : "msg-user-model-command-target" as MessageId ,
4520+ targetText : "model command thread" ,
4521+ } ) ,
4522+ } ) ;
4523+
4524+ try {
4525+ await waitForComposerEditor ( ) ;
4526+ await page . getByTestId ( "composer-editor" ) . fill ( "/mod" ) ;
4527+
4528+ const menuItem = await waitForComposerMenuItem ( "slash:model" ) ;
4529+ await menuItem . click ( ) ;
4530+
4531+ await vi . waitFor ( ( ) => {
4532+ expect ( document . querySelector ( ".model-picker-list" ) ) . not . toBeNull ( ) ;
4533+ expect ( findComposerProviderModelPicker ( ) ?. textContent ) . not . toContain ( "/model" ) ;
4534+ } ) ;
4535+
4536+ await new Promise < void > ( ( resolve ) => {
4537+ requestAnimationFrame ( ( ) => {
4538+ requestAnimationFrame ( ( ) => resolve ( ) ) ;
4539+ } ) ;
4540+ } ) ;
4541+
4542+ await vi . waitFor ( ( ) => {
4543+ const searchInput = document . querySelector < HTMLInputElement > (
4544+ 'input[placeholder="Search models..."]' ,
4545+ ) ;
4546+ expect ( searchInput ) . not . toBeNull ( ) ;
4547+ expect ( document . activeElement ) . toBe ( searchInput ) ;
4548+ } ) ;
4549+ } finally {
4550+ await mounted . cleanup ( ) ;
4551+ }
4552+ } ) ;
4553+
4554+ it ( "toggles the model picker and shows jump keys immediately from the shortcut" , async ( ) => {
4555+ const snapshot = createSnapshotForTargetUser ( {
4556+ targetMessageId : "msg-user-model-picker-shortcut-target" as MessageId ,
4557+ targetText : "model picker shortcut thread" ,
4558+ } ) ;
4559+ const mounted = await mountChatView ( {
4560+ viewport : DEFAULT_VIEWPORT ,
4561+ snapshot : {
4562+ ...snapshot ,
4563+ projects : snapshot . projects . map ( ( project ) =>
4564+ project . id === PROJECT_ID
4565+ ? Object . assign ( { } , project , {
4566+ defaultModelSelection : { provider : "codex" , model : "gpt-5.4" } ,
4567+ } )
4568+ : project ,
4569+ ) ,
4570+ threads : snapshot . threads . map ( ( thread ) =>
4571+ thread . id === THREAD_ID
4572+ ? Object . assign ( { } , thread , {
4573+ modelSelection : { provider : "codex" , model : "gpt-5.4" } ,
4574+ } )
4575+ : thread ,
4576+ ) ,
4577+ } ,
4578+ configureFixture : ( nextFixture ) => {
4579+ nextFixture . serverConfig = {
4580+ ...nextFixture . serverConfig ,
4581+ keybindings : [
4582+ {
4583+ command : "modelPicker.toggle" ,
4584+ shortcut : {
4585+ key : "m" ,
4586+ metaKey : false ,
4587+ ctrlKey : true ,
4588+ shiftKey : true ,
4589+ altKey : false ,
4590+ modKey : false ,
4591+ } ,
4592+ whenAst : {
4593+ type : "not" ,
4594+ node : { type : "identifier" , name : "terminalFocus" } ,
4595+ } ,
4596+ } ,
4597+ {
4598+ command : "thread.jump.1" ,
4599+ shortcut : {
4600+ key : "1" ,
4601+ metaKey : false ,
4602+ ctrlKey : true ,
4603+ shiftKey : false ,
4604+ altKey : false ,
4605+ modKey : false ,
4606+ } ,
4607+ } ,
4608+ {
4609+ command : "modelPicker.jump.1" ,
4610+ shortcut : {
4611+ key : "1" ,
4612+ metaKey : false ,
4613+ ctrlKey : true ,
4614+ shiftKey : false ,
4615+ altKey : false ,
4616+ modKey : false ,
4617+ } ,
4618+ whenAst : { type : "identifier" , name : "modelPickerOpen" } ,
4619+ } ,
4620+ ] ,
4621+ providers : [
4622+ {
4623+ ...nextFixture . serverConfig . providers [ 0 ] ! ,
4624+ provider : "codex" ,
4625+ models : [
4626+ {
4627+ slug : "gpt-5.1-codex-max" ,
4628+ name : "GPT-5.1 Codex Max" ,
4629+ isCustom : false ,
4630+ capabilities : {
4631+ supportsFastMode : true ,
4632+ supportsThinkingToggle : false ,
4633+ reasoningEffortLevels : [ ] ,
4634+ promptInjectedEffortLevels : [ ] ,
4635+ contextWindowOptions : [ ] ,
4636+ } ,
4637+ } ,
4638+ {
4639+ slug : "gpt-5.3-codex" ,
4640+ name : "GPT-5.3 Codex" ,
4641+ isCustom : false ,
4642+ capabilities : {
4643+ supportsFastMode : true ,
4644+ supportsThinkingToggle : false ,
4645+ reasoningEffortLevels : [ ] ,
4646+ promptInjectedEffortLevels : [ ] ,
4647+ contextWindowOptions : [ ] ,
4648+ } ,
4649+ } ,
4650+ {
4651+ slug : "gpt-5.4" ,
4652+ name : "GPT-5.4" ,
4653+ isCustom : false ,
4654+ capabilities : {
4655+ supportsFastMode : true ,
4656+ supportsThinkingToggle : false ,
4657+ reasoningEffortLevels : [ ] ,
4658+ promptInjectedEffortLevels : [ ] ,
4659+ contextWindowOptions : [ ] ,
4660+ } ,
4661+ } ,
4662+ ] ,
4663+ } ,
4664+ ] ,
4665+ } ;
4666+ } ,
4667+ } ) ;
4668+
4669+ try {
4670+ await waitForServerConfigToApply ( ) ;
4671+ await waitForComposerEditor ( ) ;
4672+
4673+ const initialPath = mounted . router . state . location . pathname ;
4674+ window . dispatchEvent (
4675+ new KeyboardEvent ( "keydown" , {
4676+ key : "m" ,
4677+ ctrlKey : true ,
4678+ shiftKey : true ,
4679+ bubbles : true ,
4680+ cancelable : true ,
4681+ } ) ,
4682+ ) ;
4683+
4684+ await vi . waitFor ( ( ) => {
4685+ expect ( document . querySelector ( ".model-picker-list" ) ) . not . toBeNull ( ) ;
4686+ } ) ;
4687+
4688+ const jumpLabel = isMacPlatform ( navigator . platform ) ? "⌃1" : "Ctrl+1" ;
4689+ await vi . waitFor ( ( ) => {
4690+ expect (
4691+ Array . from (
4692+ document . querySelectorAll < HTMLElement > ( '.model-picker-list [data-slot="kbd"]' ) ,
4693+ ) . some ( ( element ) => element . textContent ?. trim ( ) === jumpLabel ) ,
4694+ ) . toBe ( true ) ;
4695+ } ) ;
4696+ expect ( mounted . router . state . location . pathname ) . toBe ( initialPath ) ;
4697+
4698+ window . dispatchEvent (
4699+ new KeyboardEvent ( "keydown" , {
4700+ key : "m" ,
4701+ ctrlKey : true ,
4702+ shiftKey : true ,
4703+ bubbles : true ,
4704+ cancelable : true ,
4705+ } ) ,
4706+ ) ;
4707+
4708+ await vi . waitFor ( ( ) => {
4709+ expect ( document . querySelector ( ".model-picker-list" ) ) . toBeNull ( ) ;
4710+ } ) ;
4711+ } finally {
4712+ releaseModShortcut ( "Control" ) ;
4713+ await mounted . cleanup ( ) ;
4714+ }
4715+ } ) ;
4716+
44804717 it ( "shows a tooltip with the skill description when hovering a skill pill" , async ( ) => {
44814718 const mounted = await mountChatView ( {
44824719 viewport : DEFAULT_VIEWPORT ,
0 commit comments