@@ -130,25 +130,67 @@ impl DisplayField {
130130 }
131131}
132132
133- /// Render a single display field value from a task.
134- fn render_field ( field : DisplayField , task : & crate :: models:: Task ) -> Option < String > {
135- match field {
136- DisplayField :: Runtime => Some ( task. runtime . to_string ( ) ) ,
137- DisplayField :: Model => {
133+ /// Resolve the user-facing model display name for a task, accounting for runtime.
134+ ///
135+ /// * `ClaudeCode` — shorthand (opus/sonnet/haiku) as-is
136+ /// * `Codex` — explicit model ID (e.g. "o3"); `None` for Claude shorthands (default)
137+ /// * `GeminiCli` — strip "gemini-X.Y-" version prefix; "flash" for defaults/shorthands
138+ /// * `QwenCode` — always "qwen" (single model, user doesn't pick a variant)
139+ fn display_model_name ( task : & crate :: models:: Task ) -> Option < String > {
140+ use crate :: models:: AgentRuntime ;
141+ match task. runtime {
142+ AgentRuntime :: ClaudeCode => {
138143 if task. model . is_empty ( ) {
139144 None
140- } else if task. runtime != crate :: models:: AgentRuntime :: ClaudeCode {
141- // RIG-387: show runtime prefix when not claude-code so qwen/gemini/codex
142- // tasks are distinguishable from claude tasks in `werma st`.
143- // e.g. "(qwen-code/sonnet)" instead of just "(sonnet)"
144- Some ( format ! ( "{}/{}" , task. runtime, task. model) )
145145 } else {
146146 Some ( task. model . clone ( ) )
147147 }
148148 }
149+ AgentRuntime :: QwenCode => Some ( "qwen" . to_string ( ) ) ,
150+ AgentRuntime :: GeminiCli => {
151+ // Claude shorthands + empty signal "use Gemini's default" (flash)
152+ if task. model . is_empty ( ) || matches ! ( task. model. as_str( ) , "opus" | "sonnet" | "haiku" ) {
153+ Some ( "flash" . to_string ( ) )
154+ } else if let Some ( rest) = task. model . strip_prefix ( "gemini-" ) {
155+ // Skip version-like segments (digits/dots) to find the model
156+ // family name:
157+ // "2.5-flash" → skip "2.5" → "flash"
158+ // "2.5-pro" → skip "2.5" → "pro"
159+ // "2.5-flash-preview-04-17"→ skip "2.5" → "flash"
160+ // "flash" → no digits → "flash"
161+ let name = rest
162+ . split ( '-' )
163+ . find ( |s| !s. chars ( ) . all ( |c| c. is_ascii_digit ( ) || c == '.' ) )
164+ . unwrap_or ( rest) ;
165+ Some ( name. to_string ( ) )
166+ } else {
167+ Some ( task. model . clone ( ) )
168+ }
169+ }
170+ AgentRuntime :: Codex => {
171+ // Claude shorthands indicate "use Codex default" — don't show a specific name
172+ if task. model . is_empty ( ) || matches ! ( task. model. as_str( ) , "opus" | "sonnet" | "haiku" ) {
173+ None
174+ } else {
175+ Some ( task. model . clone ( ) )
176+ }
177+ }
178+ }
179+ }
180+
181+ /// Render a single display field value from a task.
182+ fn render_field ( field : DisplayField , task : & crate :: models:: Task ) -> Option < String > {
183+ match field {
184+ DisplayField :: Runtime => Some ( task. runtime . to_string ( ) ) ,
185+ DisplayField :: Model => display_model_name ( task) ,
149186 DisplayField :: Cost => task. cost_usd . map ( |c| format ! ( "${c:.2}" ) ) ,
150187 DisplayField :: Turns => {
151- if task. turns_used > 0 {
188+ if task. runtime == crate :: models:: AgentRuntime :: QwenCode {
189+ // Qwen doesn't report turns; duration is already shown in the
190+ // time column for completed tasks, so emit nothing here to
191+ // avoid duplication (e.g. "3m (qwen/3m)").
192+ None
193+ } else if task. turns_used > 0 {
152194 Some ( format ! ( "{}t" , task. turns_used) )
153195 } else {
154196 None
@@ -364,28 +406,42 @@ mod tests {
364406 assert ! ( fields. is_empty( ) ) ;
365407 }
366408
367- // ─── RIG-387: runtime prefix in Model display ────── ──────────────────
409+ // ─── RIG-399: actual model name display per runtime ──────────────────
368410
369- /// When runtime is not claude-code, Model display must include the runtime prefix.
370- ///
371- /// Bug: `render_field(Model)` returned just `task.model` ("sonnet") even for qwen/gemini
372- /// tasks, making them indistinguishable from claude-code tasks in `werma st`.
411+ /// Qwen always shows "qwen" regardless of task.model shorthand.
373412 #[ test]
374- fn display_fields_model_shows_runtime_prefix_for_non_claude ( ) {
413+ fn display_fields_qwen_shows_qwen_model_name ( ) {
375414 let task = crate :: models:: Task {
376- model : "sonnet" . into ( ) ,
415+ model : "sonnet" . into ( ) , // shorthand ignored for Qwen
377416 runtime : crate :: models:: AgentRuntime :: QwenCode ,
378- turns_used : 5 ,
417+ turns_used : 5 , // turns ignored for Qwen
379418 ..Default :: default ( )
380419 } ;
381420 let result = format_display_fields ( & task, DEFAULT_STATUS_FIELDS ) ;
382421 assert_eq ! (
383- result, " (qwen-code/sonnet/5t )" ,
384- "non-claude runtime must be prefixed to model in display "
422+ result, " (qwen)" ,
423+ "Qwen tasks must show 'qwen' as model name "
385424 ) ;
386425 }
387426
388- /// Claude-code runtime must NOT add a prefix — keep backward compatibility.
427+ /// Qwen completed tasks do NOT show duration in display fields — duration is
428+ /// already shown in the time column by the caller, so repeating it would
429+ /// produce duplicate output like "3m (qwen/3m)".
430+ #[ test]
431+ fn display_fields_qwen_completed_no_duplicate_duration ( ) {
432+ let task = crate :: models:: Task {
433+ model : "sonnet" . into ( ) ,
434+ runtime : crate :: models:: AgentRuntime :: QwenCode ,
435+ started_at : Some ( "2026-01-01T10:00:00" . into ( ) ) ,
436+ finished_at : Some ( "2026-01-01T10:03:00" . into ( ) ) ,
437+ ..Default :: default ( )
438+ } ;
439+ let result = format_display_fields ( & task, DEFAULT_STATUS_FIELDS ) ;
440+ // Only model, no duration — duration is in the time column
441+ assert_eq ! ( result, " (qwen)" ) ;
442+ }
443+
444+ /// Claude-code runtime shows shorthand as-is with turns.
389445 #[ test]
390446 fn display_fields_model_no_prefix_for_claude_code ( ) {
391447 let task = crate :: models:: Task {
@@ -401,16 +457,96 @@ mod tests {
401457 ) ;
402458 }
403459
404- /// Codex runtime also shows prefix.
460+ /// Codex with explicit model ID shows just the model (no runtime prefix) .
405461 #[ test]
406- fn display_fields_model_shows_codex_prefix ( ) {
462+ fn display_fields_codex_explicit_model_no_prefix ( ) {
407463 let task = crate :: models:: Task {
408464 model : "o4-mini" . into ( ) ,
409465 runtime : crate :: models:: AgentRuntime :: Codex ,
466+ turns_used : 12 ,
467+ ..Default :: default ( )
468+ } ;
469+ let result = format_display_fields ( & task, DEFAULT_STATUS_FIELDS ) ;
470+ assert_eq ! ( result, " (o4-mini/12t)" ) ;
471+ }
472+
473+ /// Codex with Claude shorthand (default model) shows turns only.
474+ #[ test]
475+ fn display_fields_codex_default_model_omits_model_name ( ) {
476+ let task = crate :: models:: Task {
477+ model : "sonnet" . into ( ) , // Claude shorthand = Codex default
478+ runtime : crate :: models:: AgentRuntime :: Codex ,
479+ turns_used : 8 ,
480+ ..Default :: default ( )
481+ } ;
482+ let result = format_display_fields ( & task, DEFAULT_STATUS_FIELDS ) ;
483+ assert_eq ! ( result, " (8t)" ) ;
484+ }
485+
486+ /// Gemini with Claude shorthand shows "flash" (Gemini default).
487+ #[ test]
488+ fn display_fields_gemini_default_shows_flash ( ) {
489+ let task = crate :: models:: Task {
490+ model : "sonnet" . into ( ) , // Claude shorthand → Gemini default
491+ runtime : crate :: models:: AgentRuntime :: GeminiCli ,
492+ turns_used : 10 ,
493+ ..Default :: default ( )
494+ } ;
495+ let result = format_display_fields ( & task, DEFAULT_STATUS_FIELDS ) ;
496+ assert_eq ! ( result, " (flash/10t)" ) ;
497+ }
498+
499+ /// Gemini with explicit model ID strips the "gemini-X.Y-" prefix.
500+ #[ test]
501+ fn display_fields_gemini_strips_version_prefix ( ) {
502+ let task = crate :: models:: Task {
503+ model : "gemini-2.5-flash" . into ( ) ,
504+ runtime : crate :: models:: AgentRuntime :: GeminiCli ,
505+ turns_used : 15 ,
506+ ..Default :: default ( )
507+ } ;
508+ let result = format_display_fields ( & task, DEFAULT_STATUS_FIELDS ) ;
509+ assert_eq ! ( result, " (flash/15t)" ) ;
510+ }
511+
512+ /// Gemini pro model strips correctly.
513+ #[ test]
514+ fn display_fields_gemini_pro_strips_prefix ( ) {
515+ let task = crate :: models:: Task {
516+ model : "gemini-2.5-pro" . into ( ) ,
517+ runtime : crate :: models:: AgentRuntime :: GeminiCli ,
518+ turns_used : 20 ,
519+ ..Default :: default ( )
520+ } ;
521+ let fields = & [ DisplayField :: Model ] ;
522+ let result = format_display_fields ( & task, fields) ;
523+ assert_eq ! ( result, " (pro)" ) ;
524+ }
525+
526+ /// Gemini preview/versioned models extract the family name, not a trailing
527+ /// segment like "17" from "gemini-2.5-flash-preview-04-17".
528+ #[ test]
529+ fn display_fields_gemini_preview_model_extracts_family ( ) {
530+ let task = crate :: models:: Task {
531+ model : "gemini-2.5-flash-preview-04-17" . into ( ) ,
532+ runtime : crate :: models:: AgentRuntime :: GeminiCli ,
533+ turns_used : 5 ,
534+ ..Default :: default ( )
535+ } ;
536+ let result = format_display_fields ( & task, DEFAULT_STATUS_FIELDS ) ;
537+ assert_eq ! ( result, " (flash/5t)" ) ;
538+ }
539+
540+ #[ test]
541+ fn display_fields_gemini_preview_without_date ( ) {
542+ let task = crate :: models:: Task {
543+ model : "gemini-3-flash-preview" . into ( ) ,
544+ runtime : crate :: models:: AgentRuntime :: GeminiCli ,
545+ turns_used : 7 ,
410546 ..Default :: default ( )
411547 } ;
412548 let fields = & [ DisplayField :: Model ] ;
413549 let result = format_display_fields ( & task, fields) ;
414- assert_eq ! ( result, " (codex/o4-mini )" ) ;
550+ assert_eq ! ( result, " (flash )" ) ;
415551 }
416552}
0 commit comments