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

  1. Build initialSelected as items.map(item => <whether this item should start selected>) so lengths always match.
  2. If you store selected values, convert them to the aligned array: items.map(i => savedSelections.includes(valueOf(i))).
  3. Pass undefined instead of an array when you have no prior selection (the parameter is optional).
  4. 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

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)