Coverage for src / lilbee / cli / tui / screens / catalog_grouping.py: 100%
69 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-05-15 20:55 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-05-15 20:55 +0000
1"""Row-grouping helpers and the GridSection container for CatalogScreen."""
3from __future__ import annotations
5from dataclasses import dataclass
7from lilbee.catalog.types import ModelTask
8from lilbee.cli.tui import messages as msg
9from lilbee.cli.tui.screens.catalog_utils import (
10 CatalogRow,
11 CatalogRowKind,
12 FrontierCatalogRow,
13 LocalCatalogRow,
14)
15from lilbee.cli.tui.widgets.model_list import ModelListSection
18@dataclass
19class GridSection:
20 """A named group of rows for the grid view."""
22 heading: str
23 rows: list[CatalogRow]
26TASK_BUCKET_ORDER = (ModelTask.CHAT, ModelTask.EMBEDDING, ModelTask.VISION, ModelTask.RERANK)
27PICKS_SECTION_HEADING = "★ Picks"
30def row_cache_signature(row: CatalogRow) -> tuple[str, bool]:
31 """Pair (name, installed-flag) for the per-tab cache key.
33 Frontier rows don't carry an ``installed`` field; they're keyed as
34 if installed=False since each frontier entry is provider-managed
35 rather than on-disk.
36 """
37 if row.kind == CatalogRowKind.FRONTIER:
38 return (row.name, False)
39 return (row.name, row.installed)
42def for_you_sort_key(row: LocalCatalogRow) -> tuple[int, str]:
43 """Rank Discover 'For You' rows: best fit first, then alphabetical.
45 Fit rank: FITS=0, TIGHT=1, WONT_RUN=2, no chip=3. Featured-only
46 callers already filtered, so featured isn't in the key.
47 """
48 from lilbee.runtime.hardware import FitLevel
50 if row.fit is None:
51 rank = 3
52 elif row.fit.level is FitLevel.FITS:
53 rank = 0
54 elif row.fit.level is FitLevel.TIGHT:
55 rank = 1
56 else:
57 rank = 2
58 return (rank, row.name.lower())
61def group_frontier_rows(
62 frontier_rows: list[FrontierCatalogRow],
63) -> list[ModelListSection]:
64 """Group frontier rows into provider-headed sections.
66 Section order follows :data:`PROVIDER_KEYS` (the canonical display
67 order); providers absent from PROVIDER_KEYS land at the tail in
68 alphabetical order. Rows within each section are alphabetical.
69 """
70 if not frontier_rows:
71 return []
72 from lilbee.providers.sdk_backend import PROVIDER_KEYS
74 per_provider: dict[str, list[FrontierCatalogRow]] = {}
75 for row in frontier_rows:
76 per_provider.setdefault(row.provider, []).append(row)
77 canonical_order = [label for _, _, _, label in PROVIDER_KEYS]
78 ordered = [p for p in canonical_order if p in per_provider]
79 extras = sorted(set(per_provider) - set(canonical_order))
80 sections: list[ModelListSection] = []
81 for provider in [*ordered, *extras]:
82 rows = sorted(per_provider[provider], key=lambda r: r.name.lower())
83 sections.append(ModelListSection(heading=provider, rows=list(rows)))
84 return sections
87def group_task_rows_with_picks(
88 task_rows: list[LocalCatalogRow], task_label: str
89) -> list[GridSection]:
90 """Per-tab grouping: ★ Picks pinned, then Installed, then the rest.
92 Lifts featured rows out of their task bucket into a dedicated pinned
93 section at the top of the tab. Today's behavior interleaved them at
94 the top of the task bucket; the redesign treats curation as its own
95 layer so the eye lands on Picks first instead of having to scan past
96 them to find non-featured rows.
98 Pre-condition: caller has already filtered ``task_rows`` to a single
99 task (the active per-task tab).
100 """
101 picks: list[CatalogRow] = []
102 installed: list[CatalogRow] = []
103 others: list[CatalogRow] = []
104 for row in task_rows:
105 if row.featured:
106 picks.append(row)
107 elif row.installed:
108 installed.append(row)
109 else:
110 others.append(row)
111 return [
112 GridSection(PICKS_SECTION_HEADING, picks),
113 GridSection(msg.HEADING_INSTALLED, installed),
114 GridSection(task_label, others),
115 ]
118def group_rows_for_grid(local_rows: list[LocalCatalogRow]) -> list[GridSection]:
119 """Group local rows into sections for the grid view.
121 Layout: Installed first, then one section per task. Featured rows live
122 at the top of their task section (recognizable by the ``pick`` pill);
123 no separate "Our picks" bucket so the catalog reads as a single
124 task-organized list.
125 """
126 installed: list[CatalogRow] = []
127 by_task: dict[str, list[CatalogRow]] = {task: [] for task in TASK_BUCKET_ORDER}
128 extras: dict[str, list[CatalogRow]] = {}
129 for row in local_rows:
130 if row.installed:
131 installed.append(row)
132 continue
133 bucket = by_task.get(row.task)
134 if bucket is not None:
135 bucket.append(row)
136 else:
137 extras.setdefault(row.task, []).append(row)
138 # Within each task bucket: featured first (preserving their input order),
139 # then the rest in their incoming order. Stable so HF rank from the API
140 # is preserved among non-featured rows.
141 for bucket in by_task.values():
142 bucket.sort(key=lambda r: not getattr(r, "featured", False))
143 for bucket in extras.values():
144 bucket.sort(key=lambda r: not getattr(r, "featured", False))
145 return [
146 GridSection(msg.HEADING_INSTALLED, installed),
147 *[GridSection(task.capitalize(), by_task[task]) for task in TASK_BUCKET_ORDER],
148 *[GridSection(task.capitalize(), extras[task]) for task in extras],
149 ]