Skip to content

Commit 496902a

Browse files
authored
RIG-399 feat: implementation (#238)
## Summary Pipeline engineer task `20260407-041`. Linear: RIG-399
1 parent c4b2a4e commit 496902a

2 files changed

Lines changed: 168 additions & 26 deletions

File tree

engine/src/notify.rs

Lines changed: 161 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

engine/src/ui.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,14 +496,20 @@ pub fn render_compact_buf(
496496
} else {
497497
String::new()
498498
};
499+
let cost_turns = if show_cost_turns {
500+
crate::commands::display::format_cost_turns(task, &cfg)
501+
} else {
502+
String::new()
503+
};
499504
let _ = writeln!(
500505
buf,
501-
" {} {} {}{} {}",
506+
" {} {} {}{} {}{}",
502507
green_bold(spinner),
503508
compact_task_id(&task.id),
504509
blue(compact_task_type(&task.task_type)),
505510
linear,
506511
dimmed(&elapsed),
512+
dimmed(&cost_turns),
507513
);
508514
}
509515

0 commit comments

Comments
 (0)