feat(local-model): one frame for the window, honest figures, a table for the consent

The window is one frame for the whole flow: a fixed height on desktop, App
Settings' boxed close button, the buttons in a foot of their own outside
the part that scrolls, so a click never moves the next button away and
nothing is cut off; the short screens are centred in it on desktop and sit
above the buttons on a phone.

The chooser lists the pick first and then the other models by download
size, largest first, a column the reader can see. A pick that only just
fits says "Best fit, tight". The memory figure is an estimate and says so
once, in whole GB; its legend names the peak, and the installed line uses
the same words beside what the model takes now, so the two figures are not
read as a contradiction. The legend, the column headers and the foot's
promise are set in --text-dim, the bar's track in --text-muted so its free
part can be seen, and only the download figure keeps tabular figures.

The consent screen is the download as a table: the chosen row, the runtime,
the coding agent with its installer's command as the row's second line, the
total in the strong weight, then where it goes in one line. The download bar
means the model download alone and is gone once the model is in; the screen
that ends an install shows "Start a session" alone; an error on an installed
model is the headline with a grey dot; an error's first sentence is the
headline; Remove is quiet everywhere but red in its own question. The
no-recommendation reasons are one sentence each, and the untested note is
in Codeman's voice. A refusal the server names is printed from its sentence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 23:55:02 +01:00
1 parent 3c4bb91d6f
commit 77d29d4621
9 files changed
+777 -314

No files matched your search

+7 -9
View File
@@ -313,14 +313,16 @@ function buildCandidate(
}
/**
* The one tag of a row: the first kind that applies, in the order tested, best (the pick),
* tight, slow, small. `undefined` when none does.
* The one tag of a row: the first kind that applies, in the order tested, the pick (which
* says so when it only just fits), tight, slow, small. `undefined` when none does.
*/
function tagFor(candidate: Candidate, isPick: boolean): CandidateTag | undefined {
const kind: CandidateTagKind | undefined = candidate.tested
? 'tested'
: isPick
? 'best'
? candidate.fit === 'tight'
? 'best-tight'
: 'best'
: candidate.fit === 'tight'
? 'tight'
: candidate.speed === 'slow'
@@ -442,12 +444,8 @@ export function recommend(
const hasRoom = (model: LocalModelEntry): boolean => free === undefined || free - model.bytes >= DISK_HEADROOM_BYTES;
if (free !== undefined && !accepted.some((entry) => hasRoom(entry.model))) {
const smallest = Math.min(...accepted.map((entry) => entry.model.bytes));
return none('disk', budget, {
download: formatGb(smallest),
reserve: formatGb(DISK_HEADROOM_BYTES),
free: hw.freeDiskGb ?? formatGb(free),
missing: formatGb(smallest + DISK_HEADROOM_BYTES - free),
});
// One number, the one that says what to do: what must be freed for the smallest model.
return none('disk', budget, { missing: formatGb(smallest + DISK_HEADROOM_BYTES - free) });
}
// Everything that fits is ranked, the models without room on the disk included: the best of