@ondeinference / cli / commits / 9f1b583

Address second review pass

- Bound the size-token match on both sides, not just the leading digit check, so "3B" can no longer match inside "3Beta" or "3B2" - Note why trigger_load_apps must run before app.busy is set to true in trigger_publish_model, since it early-returns when busy - Give the apps-list empty state a per-screen hint, since "Press n to create one" was showing on the publish picker where n does nothing - Add regression tests for the boundary fix Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

keypair34 committed Jul 12, 2026 at 07:52 UTC 9f1b58314de82040f00caaf48087c65c7ff2fe61
2 files changed +33 -11
src/app.rs
+23 -5
@@ -1783,7 +1783,10 @@ fn trigger_publish_model(app: &mut App, tx: mpsc::UnboundedSender<AuthEvent>) {
1783 let display_name = format!("{base_name} (Fine-tuned)");
1784 let token = token::load().unwrap_or_default();
1785
1786 - // Make sure the apps list is ready by the time the picker shows up.
1786 + // Make sure the apps list is ready by the time the picker shows up. This
1787 + // must run before `app.busy` is set below -- trigger_load_apps early-
1788 + // returns if busy is already true, so reordering these two would make
1789 + // the apps fetch silently no-op.
1790 if !app.apps_loaded {
1791 trigger_load_apps(app, tx.clone());
1792 }
@@ -1813,17 +1816,22 @@ fn trigger_publish_model(app: &mut App, tx: mpsc::UnboundedSender<AuthEvent>) {
1816 });
1817 }
1818
1816 -/// True if `size` (e.g. "3B") appears in `haystack` as its own token, not as
1817 -/// the tail of a larger number -- "3B" is a plain substring of "13B"/"43B",
1818 -/// so a naive `.contains()` would misdetect those as "3B".
1819 +/// True if `size` (e.g. "3B") appears in `haystack` as its own token, bounded
1820 +/// on both sides by a non-alphanumeric character (or the string edge). "3B"
1821 +/// is a plain substring of "13B"/"43B" (digit before) and of "3BETA"/"3B2"
1822 +/// (alphanumeric after), so a naive `.contains()` would misdetect those.
1823 fn contains_size_token(haystack: &str, size: &str) -> bool {
1824 let mut start = 0;
1825 while let Some(idx) = haystack[start..].find(size) {
1826 let abs_idx = start + idx;
1827 + let end_idx = abs_idx + size.len();
1828 let preceded_by_digit = haystack.as_bytes()[..abs_idx]
1829 .last()
1830 .is_some_and(u8::is_ascii_digit);
1826 - if !preceded_by_digit {
1831 + let followed_by_alnum = haystack.as_bytes()[end_idx..]
1832 + .first()
1833 + .is_some_and(u8::is_ascii_alphanumeric);
1834 + if !preceded_by_digit && !followed_by_alnum {
1835 return true;
1836 }
1837 start = abs_idx + 1;
@@ -3115,4 +3123,14 @@ mod tests {
3123 let (_, _, parameter_class) = derive_base_model_meta(Some("someone/Mystery-13B-GGUF"));
3124 assert_eq!(parameter_class, "unknown");
3125 }
3126 +
3127 + #[test]
3128 + fn test_derive_base_model_meta_size_not_matched_mid_word() {
3129 + // "3B" is a substring of both "3BETA" (trailing letter) and "3B2"
3130 + // (trailing digit) -- neither should resolve to "3B".
3131 + let (_, _, beta) = derive_base_model_meta(Some("someone/Mystery-3BETA-GGUF"));
3132 + assert_eq!(beta, "unknown");
3133 + let (_, _, versioned) = derive_base_model_meta(Some("someone/Mystery-3B2-GGUF"));
3134 + assert_eq!(versioned, "unknown");
3135 + }
3136 }
src/ui.rs
+10 -6
@@ -367,7 +367,7 @@ fn render_apps(frame: &mut Frame, app: &App, area: Rect) {
367 ])
368 .split(rest);
369
370 - render_apps_list(frame, app, bottom[0]);
370 + render_apps_list(frame, app, bottom[0], "No apps yet. Press n to create one.");
371
372 frame.render_widget(
373 Paragraph::new("New app name:").style(Style::new().fg(C_MUTED)),
@@ -385,7 +385,7 @@ fn render_apps(frame: &mut Frame, app: &App, area: Rect) {
385 ])
386 .split(rest);
387
388 - render_apps_list(frame, app, bottom[0]);
388 + render_apps_list(frame, app, bottom[0], "No apps yet. Press n to create one.");
389 frame.render_widget(
390 Paragraph::new("n · new Enter · open s · sign out").style(Style::new().fg(C_MUTED)),
391 bottom[1],
@@ -393,7 +393,7 @@ fn render_apps(frame: &mut Frame, app: &App, area: Rect) {
393 }
394 }
395
396 -fn render_apps_list(frame: &mut Frame, app: &App, area: Rect) {
396 +fn render_apps_list(frame: &mut Frame, app: &App, area: Rect, empty_hint: &str) {
397 if app.apps.is_empty() {
398 // Keyed on apps_loaded rather than busy: busy can flip back to
399 // false from an unrelated concurrent event (e.g. a model
@@ -406,8 +406,7 @@ fn render_apps_list(frame: &mut Frame, app: &App, area: Rect) {
406 );
407 } else {
408 frame.render_widget(
409 - Paragraph::new(" No apps yet. Press n to create one.")
410 - .style(Style::new().fg(C_MUTED)),
409 + Paragraph::new(format!(" {empty_hint}")).style(Style::new().fg(C_MUTED)),
410 area,
411 );
412 }
@@ -502,7 +501,12 @@ fn render_publish_model(frame: &mut Frame, app: &App, area: Rect) {
501 rows[3],
502 );
503
505 - render_apps_list(frame, app, rows[4]);
504 + render_apps_list(
505 + frame,
506 + app,
507 + rows[4],
508 + "No apps yet. Create one from the Apps screen, then come back to assign this model.",
509 + );
510 render_status(frame, app, rows[6]);
511 }
512