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