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