feat(cli): enforce required database selection and improve tree-picker UX (#86)

* feat(cli): enforce required database selection and improve tree-picker UX

- Require at least one database driver via prompt `required: true` instead of
  looping on empty selection; remove the now-dead retry/back-on-empty branch.
- Surface the recommended option with a "(recommended)" hint in the depth and
  query-history prompts.
- Tree picker: add `◧` partial glyph for parents whose descendants are checked,
  and make `a` toggle select-all-visible / select-none.

* fix(cli): drop unused export from tree-picker toggleSelectAllVisible

Knip flagged the export as unused; the function is only consumed by the
internal reducer via the 'toggle-select-all-visible' command, so demote
it to a module-local helper to keep CI's dead-code check green.

* test(cli): drop empty-selection warning assertion from setup test

The empty-selection retry/warning loop in `chooseDrivers` was removed in
favor of `multiselect`'s `required: true`, so the legacy warning string
is unreachable. Update the test to assert the simpler back-from-selection
return-to-embeddings flow.
This commit is contained in:
Andrey Avtomonov 2026-05-14 14:35:58 +02:00 committed by GitHub
parent e28b10454a
commit 6c4623f2ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 146 additions and 60 deletions

View file

@ -44,6 +44,7 @@ export type PickerCommand =
| 'collapse-all'
| 'toggle-check'
| 'select-all-visible'
| 'toggle-select-all-visible'
| 'select-none'
| 'clear-transient-hint'
| 'search-start'
@ -228,6 +229,17 @@ export function isAncestorChecked(nodeId: string, checked: Set<string>, byId: Ma
return ancestorsOf(nodeId, byId).some((ancestorId) => checked.has(ancestorId));
}
export function hasPartialChildren(
nodeId: string,
checked: Set<string>,
byId: Map<string, TreePickerNode>,
): boolean {
if (checked.has(nodeId) || isAncestorChecked(nodeId, checked, byId)) {
return false;
}
return descendantsOf(nodeId, byId).some((descendantId) => checked.has(descendantId));
}
function checkedAncestor(nodeId: string, state: PickerState): TreePickerNode | null {
for (const ancestorId of ancestorsOf(nodeId, state.byId)) {
if (state.checked.has(ancestorId)) {
@ -350,6 +362,16 @@ export function selectNone(state: PickerState): PickerState {
return cloneState(state, { checked: new Set(), transientHint: null });
}
function toggleSelectAllVisible(state: PickerState): PickerState {
const next = selectAllVisible(state);
const unchanged =
next.checked.size === state.checked.size && [...next.checked].every((id) => state.checked.has(id));
if (unchanged && state.checked.size > 0) {
return selectNone(state);
}
return next;
}
function setExpanded(state: PickerState, nodeId: string, value: boolean | 'toggle'): PickerState {
const expanded = new Set(state.expanded);
const nextValue = value === 'toggle' ? !expanded.has(nodeId) : value;
@ -487,6 +509,8 @@ export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()
return { next: toggleChecked(state, state.cursorId, now), effect: null };
case 'select-all-visible':
return { next: selectAllVisible(state), effect: null };
case 'toggle-select-all-visible':
return { next: toggleSelectAllVisible(state), effect: null };
case 'select-none':
return { next: selectNone(state), effect: null };
case 'clear-transient-hint':