1- import { act , createRef } from "react" ;
1+ import { act , createRef , type ComponentProps } from "react" ;
22import { createRoot , type Root } from "react-dom/client" ;
33import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
44import type { InitializeResult } from "../shared/protocol" ;
@@ -31,7 +31,18 @@ describe("RuntimePicker", () => {
3131 onToggleMenu = vi . fn ( ) ,
3232 onSelectEffort = vi . fn ( ) ,
3333 onSelectModel = vi . fn ( ) ,
34- anchorRef = createRef < HTMLDivElement > ( )
34+ anchorRef = createRef < HTMLDivElement > ( ) ,
35+ engineProps : Pick <
36+ ComponentProps < typeof RuntimePicker > ,
37+ | "engines"
38+ | "activeEngine"
39+ | "engineLocked"
40+ | "engineModel"
41+ | "engineEffort"
42+ | "onSelectEngine"
43+ | "onSelectEngineModel"
44+ | "onSelectEngineEffort"
45+ > = { }
3546 ) : void {
3647 act ( ( ) => {
3748 root ??= createRoot ( container ) ;
@@ -46,6 +57,7 @@ describe("RuntimePicker", () => {
4657 onToggleMenu = { onToggleMenu }
4758 onSelectModel = { onSelectModel }
4859 onSelectEffort = { onSelectEffort }
60+ { ...engineProps }
4961 />
5062 ) ;
5163 } ) ;
@@ -89,45 +101,114 @@ describe("RuntimePicker", () => {
89101 expect ( onToggleMenu ) . toHaveBeenCalledWith ( "model" ) ;
90102 } ) ;
91103
92- it ( "opens the model panel directly with search, provider groups, and the effort slider " , ( ) => {
104+ it ( "opens as a compact summary and drills into the model list " , ( ) => {
93105 renderPicker ( "model" , runtimeWithEffort ( ) ) ;
94106
95107 const menu = document . querySelector < HTMLElement > ( ".codex-model-menu" ) ;
96108 expect ( menu ) . not . toBeNull ( ) ;
97109 // The intermediate main menu is gone.
98110 expect ( document . querySelector ( ".codex-main-menu" ) ) . toBeNull ( ) ;
99111
112+ expect ( menu ?. classList . contains ( "is-summary" ) ) . toBe ( true ) ;
113+ expect ( menu ?. textContent ) . toContain ( "Wuu" ) ;
114+ expect ( menu ?. textContent ) . toContain ( "work" ) ;
115+ expect ( menu ?. textContent ) . toContain ( "Claude Sonnet" ) ;
116+ expect ( menu ?. textContent ) . toContain ( "Medium" ) ;
117+ expect ( menu ?. querySelector ( ".select-menu-search input" ) ) . toBeNull ( ) ;
118+ const effortSlider = menu ?. querySelector < HTMLInputElement > ( '.codex-effort-slider input[type="range"]' ) ;
119+ expect ( effortSlider ?. value ) . toBe ( "2" ) ;
120+ expect ( effortSlider ?. getAttribute ( "aria-valuetext" ) ) . toBe ( "Medium" ) ;
121+ expect ( menu ?. querySelector ( ".codex-effort-slider" ) ?. textContent ) . toBe ( "" ) ;
122+
123+ act ( ( ) => menu ?. querySelector < HTMLButtonElement > ( ".runtime-panel-model" ) ?. click ( ) ) ;
124+
100125 const search = menu ?. querySelector < HTMLInputElement > ( ".select-menu-search input" ) ;
101- expect ( search ) . not . toBeNull ( ) ;
102126 expect ( search ?. placeholder ) . toBe ( "搜索模型" ) ;
103-
104- const groupLabels = Array . from ( menu ?. querySelectorAll < HTMLElement > ( ".codex-model-group-label" ) ?? [ ] ) ;
105- expect ( groupLabels . map ( ( label ) => label . textContent ) ) . toEqual ( [ "work" ] ) ;
106127 const modelItems = Array . from ( menu ?. querySelectorAll < HTMLButtonElement > ( ".codex-model-item" ) ?? [ ] ) ;
107128 expect ( modelItems . map ( ( item ) => item . textContent ?. trim ( ) ) ) . toEqual ( [ "Claude Sonnet" ] ) ;
108129 expect ( modelItems [ 0 ] ?. getAttribute ( "aria-checked" ) ) . toBe ( "true" ) ;
130+ } ) ;
109131
110- const slider = menu ?. querySelector < HTMLInputElement > ( ".codex-effort-slider" ) ;
111- expect ( slider ?. type ) . toBe ( "range" ) ;
112- expect ( slider ?. min ) . toBe ( "0" ) ;
113- expect ( slider ?. max ) . toBe ( "3" ) ;
114- expect ( slider ?. value ) . toBe ( "2" ) ;
115- expect ( menu ?. textContent ) . toContain ( "Medium" ) ;
132+ it ( "presents engines as the parent navigation for the selected engine's models" , ( ) => {
133+ const onSelectEngine = vi . fn ( ) ;
134+ renderPicker (
135+ "model" ,
136+ runtimeWithEffort ( ) ,
137+ vi . fn ( ) ,
138+ vi . fn ( ) ,
139+ vi . fn ( ) ,
140+ createRef < HTMLDivElement > ( ) ,
141+ {
142+ engines : [
143+ { id : "wuu" , enabled : true , binary_ok : true } ,
144+ {
145+ id : "codex" ,
146+ enabled : true ,
147+ binary_ok : true ,
148+ models : [
149+ {
150+ id : "gpt-5.6-sol" ,
151+ display_name : "GPT-5.6-Sol" ,
152+ supported_efforts : [ "low" , "medium" , "high" ]
153+ }
154+ ]
155+ }
156+ ] ,
157+ activeEngine : "codex" ,
158+ engineModel : "gpt-5.6-sol" ,
159+ engineEffort : "low" ,
160+ onSelectEngine,
161+ onSelectEngineModel : vi . fn ( ) ,
162+ onSelectEngineEffort : vi . fn ( )
163+ }
164+ ) ;
165+
166+ const engineContext = Array . from ( document . querySelectorAll < HTMLButtonElement > ( ".runtime-panel-context button" ) )
167+ . find ( ( button ) => button . textContent ?. includes ( "Codex" ) ) ;
168+ act ( ( ) => engineContext ?. click ( ) ) ;
169+ const engineRail = document . querySelector < HTMLElement > ( '.runtime-engine-options[role="group"]' ) ;
170+ expect ( engineRail ?. getAttribute ( "aria-label" ) ) . toBe ( "运行引擎" ) ;
171+ expect ( engineRail ?. textContent ) . not . toContain ( "Agent" ) ;
172+ const engineChoices = Array . from (
173+ engineRail ?. querySelectorAll < HTMLButtonElement > ( '[role="menuitemradio"]' ) ?? [ ]
174+ ) ;
175+ expect ( engineChoices . map ( ( choice ) => choice . querySelector ( ".runtime-engine-option-name" ) ?. textContent ) ) . toEqual ( [
176+ "Wuu" ,
177+ "Codex"
178+ ] ) ;
179+ expect ( engineChoices [ 1 ] ?. getAttribute ( "aria-checked" ) ) . toBe ( "true" ) ;
180+ act ( ( ) => engineChoices [ 0 ] ?. click ( ) ) ;
181+ expect ( onSelectEngine ) . toHaveBeenCalledWith ( "wuu" ) ;
182+ } ) ;
183+
184+ it ( "keeps every engine visible when the current conversation locks engine switching" , ( ) => {
185+ renderPicker (
186+ "model" ,
187+ runtimeWithEffort ( ) ,
188+ vi . fn ( ) ,
189+ vi . fn ( ) ,
190+ vi . fn ( ) ,
191+ createRef < HTMLDivElement > ( ) ,
192+ {
193+ engines : [
194+ { id : "wuu" , enabled : true , binary_ok : true } ,
195+ { id : "codex" , enabled : true , binary_ok : true }
196+ ] ,
197+ activeEngine : "codex" ,
198+ engineLocked : true
199+ }
200+ ) ;
116201
117- // The capsule is a bare pill: no level text on it — the heading above
118- // live-shows the current level, and small dots mark the available stops.
119- expect ( menu ?. querySelector ( ".codex-effort-marks" ) ) . toBeNull ( ) ;
120- expect ( menu ?. querySelector ( ".codex-effort-mark" ) ) . toBeNull ( ) ;
121- expect ( menu ?. querySelector ( ".codex-effort-current" ) ?. textContent ) . toBe ( "Medium" ) ;
122- expect ( menu ?. querySelectorAll ( ".codex-effort-stop" ) ) . toHaveLength ( 3 ) ;
123- const wrap = menu ?. querySelector < HTMLElement > ( ".codex-effort-slider-wrap" ) ;
124- expect ( wrap ?. style . getPropertyValue ( "--effort-slider-fill" ) ) . toBe ( "75%" ) ;
125- expect ( wrap ?. style . getPropertyValue ( "--effort-slider-pos" ) ) . toBe ( "75%" ) ;
126- // The drag pearl sits on the current stop; mid-scale is not charged.
127- const capsule = menu ?. querySelector < HTMLElement > ( ".codex-effort-capsule" ) ;
128- expect ( capsule ?. querySelector ( ".codex-effort-knob" ) ) . not . toBeNull ( ) ;
129- expect ( capsule ?. classList . contains ( "maxed" ) ) . toBe ( false ) ;
130- expect ( capsule ?. querySelector ( ".codex-effort-capsule-sheen" ) ) . toBeNull ( ) ;
202+ const engineContext = document . querySelector < HTMLButtonElement > ( ".runtime-panel-context button" ) ;
203+ act ( ( ) => engineContext ?. click ( ) ) ;
204+ const choices = Array . from (
205+ document . querySelectorAll < HTMLButtonElement > ( ".runtime-engine-option" )
206+ ) ;
207+ expect ( choices . map ( ( choice ) => choice . querySelector ( ".runtime-engine-option-name" ) ?. textContent ) ) . toEqual ( [
208+ "Wuu" ,
209+ "Codex"
210+ ] ) ;
211+ expect ( choices . every ( ( choice ) => choice . disabled ) ) . toBe ( true ) ;
131212 } ) ;
132213
133214 it ( "builds permission labels in the active language" , ( ) => {
@@ -154,14 +235,15 @@ describe("RuntimePicker", () => {
154235 } ,
155236 ) ;
156237
157- it ( "hides the effort slider when the model does not expose reasoning levels" , ( ) => {
238+ it ( "hides effort choices when the model does not expose reasoning levels" , ( ) => {
158239 const initialized = runtimeWithEffort ( ) ;
159240 initialized . variant = "" ;
160241 initialized . providers ! [ 0 ] . models = [ { id : "claude-sonnet" , display_name : "Claude Sonnet" } ] ;
161242
162243 renderPicker ( "model" , initialized ) ;
163244
164245 expect ( document . querySelector ( ".codex-effort-slider" ) ) . toBeNull ( ) ;
246+ act ( ( ) => document . querySelector < HTMLButtonElement > ( ".runtime-panel-model" ) ?. click ( ) ) ;
165247 const menu = document . querySelector < HTMLElement > ( ".codex-model-menu" ) ;
166248 expect ( menu ?. querySelectorAll ( ".codex-model-item" ) ) . toHaveLength ( 1 ) ;
167249 } ) ;
@@ -174,6 +256,8 @@ describe("RuntimePicker", () => {
174256 ] ;
175257 renderPicker ( "model" , initialized ) ;
176258
259+ act ( ( ) => document . querySelector < HTMLButtonElement > ( ".runtime-panel-model" ) ?. click ( ) ) ;
260+
177261 const search = document . querySelector < HTMLInputElement > ( ".select-menu-search input" ) ! ;
178262 const setSearchValue = ( value : string ) : void => {
179263 const valueSetter = Object . getOwnPropertyDescriptor (
@@ -192,28 +276,64 @@ describe("RuntimePicker", () => {
192276 expect ( document . querySelector ( ".composer-menu-empty" ) ?. textContent ) . toBe ( "没有匹配的模型" ) ;
193277 } ) ;
194278
195- it ( "previews the dragged effort in the heading and commits once on release" , ( ) => {
279+ it ( "makes every Wuu provider directly selectable without burying its models in one long list" , ( ) => {
280+ const initialized = runtimeWithEffort ( ) ;
281+ initialized . provider = "tokenhub" ;
282+ initialized . model = "gpt-5.6-sol" ;
283+ initialized . providers = [
284+ {
285+ name : "deepseek" ,
286+ type : "openai-compatible" ,
287+ model : "deepseek-chat" ,
288+ models : [ { id : "deepseek-chat" , display_name : "DeepSeek Chat" } ]
289+ } ,
290+ {
291+ name : "tokenhub" ,
292+ type : "openai-compatible" ,
293+ model : "gpt-5.6-sol" ,
294+ models : [
295+ { id : "gpt-5.6-sol" , display_name : "GPT-5.6 Sol" } ,
296+ { id : "gpt-5.6-terra" , display_name : "GPT-5.6 Terra" } ,
297+ { id : "gpt-5.6-luna" , display_name : "GPT-5.6 Luna" }
298+ ]
299+ }
300+ ] ;
301+
302+ renderPicker ( "model" , initialized ) ;
303+
304+ const providerContext = Array . from ( document . querySelectorAll < HTMLButtonElement > ( ".runtime-panel-context button" ) )
305+ . find ( ( button ) => button . textContent ?. includes ( "tokenhub" ) ) ;
306+ act ( ( ) => providerContext ?. click ( ) ) ;
307+
308+ const providerOptions = Array . from ( document . querySelectorAll < HTMLButtonElement > ( ".runtime-provider-option" ) ) ;
309+ expect ( providerOptions . map ( ( option ) => option . textContent ?. trim ( ) ) ) . toEqual ( [ "deepseek" , "tokenhub" ] ) ;
310+ expect ( providerOptions [ 1 ] ?. getAttribute ( "aria-checked" ) ) . toBe ( "true" ) ;
311+
312+ act ( ( ) => providerOptions [ 0 ] ?. click ( ) ) ;
313+ expect ( document . querySelector ( ".runtime-panel-context" ) ?. textContent ) . toContain ( "deepseek" ) ;
314+ act ( ( ) => document . querySelector < HTMLButtonElement > ( ".runtime-panel-model" ) ?. click ( ) ) ;
315+ expect ( document . querySelector ( ".codex-model-item-name" ) ?. textContent ) . toBe ( "DeepSeek Chat" ) ;
316+ } ) ;
317+
318+ it ( "selects a discrete effort by dragging the unlabeled slider" , ( ) => {
196319 const onSelectEffort = vi . fn ( ) ;
197320 renderPicker ( "model" , runtimeWithEffort ( ) , vi . fn ( ) , onSelectEffort ) ;
198321
199- const slider = document . querySelector < HTMLInputElement > ( " .codex-effort-slider" ) ! ;
322+ const slider = document . querySelector < HTMLInputElement > ( ' .codex-effort-slider input[type="range"]' ) ! ;
200323 act ( ( ) => {
201- const valueSetter = Object . getOwnPropertyDescriptor (
202- HTMLInputElement . prototype ,
203- "value" ,
204- ) ?. set ;
205- valueSetter ?. call ( slider , "3" ) ;
324+ Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , "value" ) ?. set ?. call ( slider , "3" ) ;
206325 slider . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
207- slider . dispatchEvent ( new Event ( "pointerup" , { bubbles : true } ) ) ;
208326 } ) ;
209327
328+ expect ( document . querySelector ( ".runtime-panel-effort-value" ) ?. textContent ) . toBe ( "High" ) ;
329+ expect ( onSelectEffort ) . not . toHaveBeenCalled ( ) ;
330+
331+ act ( ( ) => slider . dispatchEvent ( new Event ( "pointerup" , { bubbles : true } ) ) ) ;
332+
210333 expect ( onSelectEffort ) . toHaveBeenCalledTimes ( 1 ) ;
211334 expect ( onSelectEffort ) . toHaveBeenCalledWith ( "high" ) ;
212- expect ( document . querySelector ( ".codex-effort-current" ) ?. textContent ) . toBe ( "High" ) ;
213- // Landing on the top level switches on the charged state.
214- const capsule = document . querySelector ( ".codex-effort-capsule" ) ;
215- expect ( capsule ?. classList . contains ( "maxed" ) ) . toBe ( true ) ;
216- expect ( capsule ?. querySelector ( ".codex-effort-capsule-sheen" ) ) . not . toBeNull ( ) ;
335+ expect ( slider . value ) . toBe ( "3" ) ;
336+ expect ( slider . getAttribute ( "aria-valuetext" ) ) . toBe ( "High" ) ;
217337 } ) ;
218338
219339 it ( "flips the model menu below the trigger when the window top has too little room" , ( ) => {
@@ -238,7 +358,7 @@ describe("RuntimePicker", () => {
238358 '[data-floating-menu-owner="codex-runtime"]'
239359 ) ;
240360 expect ( layer ?. classList . contains ( "floating-menu-below" ) ) . toBe ( true ) ;
241- expect ( layer ?. style . left ) . toBe ( "620px " ) ;
361+ expect ( layer ?. style . left ) . toBe ( "560px " ) ;
242362 expect ( layer ?. style . top ) . toBe ( "78px" ) ;
243363 expect ( layer ?. style . bottom ) . toBe ( "" ) ;
244364 expect ( layer ?. style . getPropertyValue ( "--floating-menu-available-height" ) ) . toBe (
@@ -269,17 +389,19 @@ describe("RuntimePicker", () => {
269389
270390 renderPicker ( "model" , initialized , vi . fn ( ) , vi . fn ( ) , onSelectModel ) ;
271391
392+ act ( ( ) => document . querySelector < HTMLButtonElement > ( ".runtime-panel-model" ) ?. click ( ) ) ;
393+
272394 const choices = Array . from (
273395 document . querySelectorAll < HTMLButtonElement > ( ".codex-model-menu .codex-model-item" )
274396 ) ;
275397 const modelB = choices . find ( ( choice ) => choice . textContent ?. includes ( "Model B" ) ) ! ;
276398 act ( ( ) => modelB ?. click ( ) ) ;
277399
278400 expect ( onSelectModel ) . toHaveBeenCalledWith ( "work" , "model-b" , "medium" ) ;
279- // Optimistic in-panel state: the clicked row highlights and the slider
280- // snaps to the target model's default before the stream round-trip.
281- expect ( modelB . getAttribute ( "aria-checked" ) ) . toBe ( "true " ) ;
282- const slider = document . querySelector < HTMLInputElement > ( " .codex-effort-slider" ) ;
283- expect ( slider ?. value ) . toBe ( "1 " ) ;
401+ // The picker returns to the compact summary and previews the target
402+ // model's own default before the stream round-trip.
403+ expect ( document . querySelector ( ".runtime-panel-model-name" ) ?. textContent ) . toBe ( "Model B " ) ;
404+ const selectedEffort = document . querySelector < HTMLInputElement > ( ' .codex-effort-slider input[type="range"]' ) ;
405+ expect ( selectedEffort ?. getAttribute ( "aria-valuetext" ) ) . toBe ( "Medium " ) ;
284406 } ) ;
285407} ) ;
0 commit comments