midudev/autoskills · error · Error
initialSelected length
Error message
initialSelected length (${initialSelected.length}) must match items length (${items.length}) What it means
The multiSelect UI helper requires initialSelected to be a per-item boolean-selection map expressed positionally: it must have exactly one entry per item in items. Passing an array of different length is a programming error because selections cannot be meaningfully aligned to items, so the function fails fast.
Solutions
- Build initialSelected as items.map(item => <whether this item should start selected>) so lengths always match.
- If you store selected values, convert them to the aligned array: items.map(i => savedSelections.includes(valueOf(i))).
- Pass undefined instead of an array when you have no prior selection (the parameter is optional).
- Recompute the selection array at call time from the current items rather than caching it.
Example fix
// before
const initialSelected = previouslyChosenSkills; // array of chosen items, wrong length
await multiSelect(allSkills, { initialSelected });
// after
const initialSelected = allSkills.map(s => previouslyChosenSkills.includes(s));
await multiSelect(allSkills, { initialSelected }); Defensive patterns
Strategy: validation
Validate before calling
function alignedSelection(items, chosenValues, valueOf) {
const set = new Set(chosenValues);
return items.map(i => set.has(valueOf(i)));
}
// call-site guard
const initialSelected = alignedSelection(allSkills, savedSkillIds, s => s.id);
if (initialSelected.length !== allSkills.length) throw new Error("selection misaligned"); Type guard
const isAlignedSelection = <T,>(items: T[], sel?: unknown[]): sel is boolean[] => Array.isArray(sel) && sel.every(v => typeof v === "boolean") && sel.length === items.length;
Try / catch
try {
return await multiSelect(items, { initialSelected });
} catch (e) {
if (e.message.includes("must match items length")) {
return multiSelect(items, {}); // degrade to no preselection instead of crashing
} else throw e;
} Prevention
- Always derive initialSelected from the current items array via items.map(...).
- Never cache selection masks across runs where the item list can change.
- Omit initialSelected entirely when there is no prior state.
- Add a unit test asserting selection array length equals items length for each list state.
When it happens
Trigger: Calling multiSelect(items, { initialSelected }) where initialSelected.length !== items.length — e.g. preselecting by filtering 'selected item objects' instead of building a boolean array aligned to the items list, or reusing a stale selection array after the items list changed.
Common situations: Persisting user selections and passing them back after the item list was re-filtered or re-sorted; confusing 'list of selected values' with the per-item selection mask the API expects; dynamic skill lists changing length between runs.
AI-assisted analysis of midudev/autoskills@0ec725320d (2026-09-15).
Data as JSON: /api/errors/a286d957d61aeb4f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/autoskills/ui.ts:102
write(dim(` ${subtitle}`) + "\n");
write(SHOW_CURSOR);
log();
}
interface MultiSelectOptions<T> {
labelFn: (item: T, i: number) => string;
hintFn?: (item: T, i: number) => string;
groupFn?: (item: T) => string;
initialSelected?: boolean[];
shortcuts?: { key: string; label: string; fn: (items: T[]) => boolean[] }[];
}
export function multiSelect<T>(
items: T[],
{ labelFn, hintFn, groupFn, initialSelected, shortcuts = [] }: MultiSelectOptions<T>,
): Promise<T[]> {
if (initialSelected && initialSelected.length !== items.length) {
throw new Error(
`initialSelected length (${initialSelected.length}) must match items length (${items.length})`,
);
}
if (!process.stdin.isTTY) return Promise.resolve(items);
return new Promise((resolve) => {
const selected = initialSelected
? initialSelected.slice()
: Array.from({ length: items.length }, () => true);
let cursor = 0;
let rendered = false;
let groupCount = 0;
if (groupFn) {
let last: string | null = null;
for (const item of items) {
const g = groupFn(item);View on GitHub (pinned to 0ec725320d)