@modelstatus/cli 0.1.43 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/tui/views/inventory.js +49 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -6,8 +6,8 @@ import React from "react";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Box, Text, useInput } from "ink";
|
|
8
8
|
import {
|
|
9
|
-
h, C, GLYPH, ListRow, StateLine, EmptyCard, healthColor, healthGlyph,
|
|
10
|
-
relativeTime, envTag, cell, cellE, snippetLines, SPINNER, useTick, useAsync, clampCursor,
|
|
9
|
+
h, C, GLYPH, ListRow, StateLine, EmptyCard, SearchBar, healthColor, healthGlyph,
|
|
10
|
+
relativeTime, envTag, cell, cellE, snippetLines, SPINNER, useTick, useAsync, useSearch, clampCursor,
|
|
11
11
|
} from "../ui.js";
|
|
12
12
|
import { readSnippet, findSourceFile } from "../snippet.js";
|
|
13
13
|
import { sourceOf, SOURCE_META, sourceGlyph } from "../source-meta.js";
|
|
@@ -17,6 +17,7 @@ const ENV_ORDER = ["prod", "staging", "dev", "unknown"];
|
|
|
17
17
|
export const meta = {
|
|
18
18
|
keys: [
|
|
19
19
|
{ k: "↑↓", label: "nav" },
|
|
20
|
+
{ k: "/", label: "search" },
|
|
20
21
|
{ k: "e", label: "env" },
|
|
21
22
|
{ k: "t", label: "tag untagged" },
|
|
22
23
|
{ k: "c", label: "critical" },
|
|
@@ -44,7 +45,27 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
44
45
|
const usages = q.data?.usages || [];
|
|
45
46
|
const projects = q.data?.projects || [];
|
|
46
47
|
const projName = (id) => projects.find((p) => p.id === id)?.name || "—";
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
// Filter (press / ) — matches the model, the SOURCE (badge + label + scheme),
|
|
50
|
+
// the project + env. So "vercel"/"supabase"/"aws"/"github"/"repo"/"manual"
|
|
51
|
+
// search BY SOURCE, while "gpt" / a project name still work. Reuses the Scan
|
|
52
|
+
// tab's search hook so the / + esc + type flow is identical everywhere.
|
|
53
|
+
const search = useSearch();
|
|
54
|
+
const query = search.query.trim().toLowerCase();
|
|
55
|
+
const haystack = (u) => {
|
|
56
|
+
const st = sourceOf(u.location_label, u.discovered_by);
|
|
57
|
+
const m = SOURCE_META[st] || {};
|
|
58
|
+
return [u.model_display, u.custom_model_name, u.canonical_id, m.badge, m.label, st, projName(u.project_id), u.environment]
|
|
59
|
+
.map((x) => String(x || "").toLowerCase())
|
|
60
|
+
.join(" ");
|
|
61
|
+
};
|
|
62
|
+
const filtered = query ? usages.filter((u) => haystack(u).includes(query)) : usages;
|
|
63
|
+
const cur = filtered[clampCursor(cursor, filtered.length)] || null;
|
|
64
|
+
|
|
65
|
+
// Keep the cursor in range as the filter narrows; let the shell route backspace
|
|
66
|
+
// to "clear filter" (not "previous tab") while a query is active.
|
|
67
|
+
React.useEffect(() => { setCursor(0); }, [query]);
|
|
68
|
+
React.useEffect(() => { ui?.setHandlesBack?.(!!search.query); }, [search.query, ui]);
|
|
48
69
|
|
|
49
70
|
// Drill-down code preview for the highlighted usage's source location — same
|
|
50
71
|
// syntax-highlighted snippet as the Here tab, shown below the list (the side
|
|
@@ -67,8 +88,8 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
67
88
|
React.useEffect(() => {
|
|
68
89
|
const counts = { ok: 0, deprecating: 0, retiring: 0, retired: 0, custom: 0 };
|
|
69
90
|
for (const u of usages) if (counts[u.health] != null) counts[u.health] += 1;
|
|
70
|
-
ui?.reportStatus?.({ counts, context: `${usages.length} tracked` });
|
|
71
|
-
}, [usages, ui]);
|
|
91
|
+
ui?.reportStatus?.({ counts, context: query ? `${filtered.length} of ${usages.length}` : `${usages.length} tracked` });
|
|
92
|
+
}, [usages, ui, query, filtered.length]);
|
|
72
93
|
|
|
73
94
|
async function patch(u, body, label) {
|
|
74
95
|
try {
|
|
@@ -83,8 +104,18 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
83
104
|
useInput(
|
|
84
105
|
(input, key) => {
|
|
85
106
|
if (!active) return;
|
|
86
|
-
|
|
87
|
-
if (
|
|
107
|
+
// --- filter mode: type to search (matches model + SOURCE + project + env) ---
|
|
108
|
+
if (search.isSearchingNow()) {
|
|
109
|
+
if (key.escape) { search.clear(); setCursor(0); ui?.setCapturing?.(false); return; }
|
|
110
|
+
if (key.return) { search.confirm(); ui?.setCapturing?.(false); return; }
|
|
111
|
+
if (key.backspace || key.delete) return search.backspace();
|
|
112
|
+
if (input && !key.ctrl && !key.meta) return search.type(input);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (input === "/") { search.open(); ui?.setCapturing?.(true); return; }
|
|
116
|
+
if ((key.escape || key.backspace || key.delete) && search.query) { search.clear(); setCursor(0); return; }
|
|
117
|
+
if (key.downArrow || input === "j") return setCursor((c) => clampCursor(c + 1, filtered.length));
|
|
118
|
+
if (key.upArrow || input === "k") return setCursor((c) => clampCursor(c - 1, filtered.length));
|
|
88
119
|
if (input === "g") return q.reload();
|
|
89
120
|
if (input === "r") return ui.switchTo("scan");
|
|
90
121
|
if (input === "n") return ui.switchTo("add");
|
|
@@ -135,7 +166,7 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
135
166
|
.deleteUsage(cur.id)
|
|
136
167
|
.then(() => {
|
|
137
168
|
ui.showToast("deleted");
|
|
138
|
-
setCursor((c) => clampCursor(c,
|
|
169
|
+
setCursor((c) => clampCursor(c, filtered.length - 1));
|
|
139
170
|
q.reload();
|
|
140
171
|
})
|
|
141
172
|
.catch((e) => ui.showToast(e.message, "red"));
|
|
@@ -184,10 +215,11 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
184
215
|
const showNote = !showSnip && !!srcRef && height >= 14; // has a path but no local file
|
|
185
216
|
const reserve = showSnip ? SNIP : showNote ? 2 : 0;
|
|
186
217
|
const topH = height - reserve;
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
218
|
+
const searchRow = search.active ? 1 : 0; // the / search bar costs a row when shown
|
|
219
|
+
const VISIBLE = Math.max(3, topH - 2 - searchRow); // header + "…and N more" overhead
|
|
220
|
+
const c = clampCursor(cursor, filtered.length);
|
|
221
|
+
const start = Math.max(0, Math.min(c - Math.floor(VISIBLE / 2), Math.max(0, filtered.length - VISIBLE)));
|
|
222
|
+
const view = filtered.slice(start, start + VISIBLE);
|
|
191
223
|
|
|
192
224
|
const header = h(
|
|
193
225
|
Text,
|
|
@@ -215,13 +247,15 @@ export function InventoryView({ client, ui, dir = ".", active, width = 78, heigh
|
|
|
215
247
|
return h(ListRow, { key: u.id, active: isCur, cells, width: leftWidth });
|
|
216
248
|
});
|
|
217
249
|
|
|
250
|
+
const noMatches = query && filtered.length === 0;
|
|
218
251
|
const list = h(
|
|
219
252
|
Box,
|
|
220
253
|
{ flexDirection: "column", width: leftWidth },
|
|
254
|
+
search.active ? h(SearchBar, { searching: search.searching, query: search.query, count: filtered.length }) : null,
|
|
221
255
|
header,
|
|
222
|
-
...rowNodes,
|
|
223
|
-
|
|
224
|
-
? h(Text, { color: C.FG_FAINT }, ` ${start + 1}-${Math.min(start + VISIBLE,
|
|
256
|
+
...(noMatches ? [h(Text, { color: C.FG_DIM }, ` no matches for "${search.query}"`)] : rowNodes),
|
|
257
|
+
filtered.length > VISIBLE
|
|
258
|
+
? h(Text, { color: C.FG_FAINT }, ` ${start + 1}-${Math.min(start + VISIBLE, filtered.length)} of ${filtered.length}`)
|
|
225
259
|
: null,
|
|
226
260
|
);
|
|
227
261
|
|