@textui/widgets 0.5.0 → 0.6.1

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.
@@ -58,20 +58,6 @@ export interface CommandPaletteProps extends BoxProps {
58
58
  */
59
59
  openAt?: string;
60
60
  }
61
- /**
62
- * The command palette.
63
- *
64
- * It searches the command registry rather than a list someone maintained, so
65
- * a command registered anywhere is reachable here the moment it exists - which
66
- * is the payoff for insisting actions are commands. It also *runs* what it
67
- * finds, so choosing "Open dialog" here and pressing the button that opens a
68
- * dialog are the same act, reaching the same code.
69
- *
70
- * A command that declares an argument with `choices` gets a second level:
71
- * choosing it lists the choices, and picking one runs the command with it.
72
- * That is where sub-items come from - the command says what it needs and the
73
- * palette asks, rather than every caller inventing its own submenu.
74
- */
75
61
  export declare const CommandPalette: (props: CommandPaletteProps) => import("@textui/core").RenderOutput;
76
62
  /**
77
63
  * The argument the palette should ask about, if there is one.
@@ -1 +1 @@
1
- {"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../src/overlay/command-palette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAa,MAAM,cAAc,CAAC;AAiB/F,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACnD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,qEA2ZzB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,EAC1B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,GAAG,SAAS,CAKrB;AAqBD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAehG"}
1
+ {"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../src/overlay/command-palette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAa,MAAM,cAAc,CAAC;AAiB/F,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACnD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyBD,eAAO,MAAM,cAAc,qEAqczB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,EAC1B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,GAAG,SAAS,CAKrB;AAyCD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAehG"}
@@ -2,20 +2,6 @@ import { defineComponent, h, stringWidth, useEffect, useFocusScope, useInput, us
2
2
  import { TextInput } from '../control/index.js';
3
3
  import { Menu } from '../navigation/index.js';
4
4
  import { hint } from './shared.js';
5
- /**
6
- * The command palette.
7
- *
8
- * It searches the command registry rather than a list someone maintained, so
9
- * a command registered anywhere is reachable here the moment it exists - which
10
- * is the payoff for insisting actions are commands. It also *runs* what it
11
- * finds, so choosing "Open dialog" here and pressing the button that opens a
12
- * dialog are the same act, reaching the same code.
13
- *
14
- * A command that declares an argument with `choices` gets a second level:
15
- * choosing it lists the choices, and picking one runs the command with it.
16
- * That is where sub-items come from - the command says what it needs and the
17
- * palette asks, rather than every caller inventing its own submenu.
18
- */
19
5
  export const CommandPalette = defineComponent('CommandPalette', (props) => {
20
6
  const theme = useTheme();
21
7
  const runtime = useRuntime();
@@ -99,10 +85,29 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
99
85
  ? { sectionBefore: command.category }
100
86
  : {}),
101
87
  }));
88
+ /** The argument answered last, when this question came after one. */
89
+ const previousOf = (asked) => {
90
+ const last = Object.keys(asked.collected).at(-1);
91
+ return last === undefined ? undefined : (asked.command.args ?? []).find((arg) => arg.name === last);
92
+ };
93
+ /**
94
+ * One question back.
95
+ *
96
+ * After a first question the second is asked again from the start with
97
+ * the earlier answer withdrawn: a wrong provider is corrected by choosing
98
+ * another, not by closing the whole picker and opening it again. At the
99
+ * first question, the command list.
100
+ */
102
101
  const back = () => {
103
102
  // `null` is "never mind": whatever the preview did gets undone by whoever
104
103
  // did it, because only the command knows what it changed.
105
104
  pending?.arg.preview?.(null);
105
+ const previous = pending ? previousOf(pending) : undefined;
106
+ if (pending && previous) {
107
+ const { [previous.name]: _withdrawn, ...rest } = pending.collected;
108
+ drillInto(pending.command, previous, rest);
109
+ return;
110
+ }
106
111
  setPending(null);
107
112
  setChoices([]);
108
113
  setQuery('');
@@ -144,9 +149,16 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
144
149
  setRefresh((n) => n + 1);
145
150
  onRun?.(id, args);
146
151
  };
147
- /** Ask about one command. `choices` may be a function, and may be async. */
152
+ /**
153
+ * Ask about one command. `choices` may be a function, and may be async.
154
+ *
155
+ * A resolver is handed what was answered before this argument - and an
156
+ * argument that was not asked because its `default` stood was answered by
157
+ * that default. Left out, "the models of the chosen provider" would see no
158
+ * provider exactly when there was only one to choose from.
159
+ */
148
160
  const drillInto = (command, arg, collected = {}) => {
149
- const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
161
+ const resolved = typeof arg.choices === 'function' ? arg.choices(answeredBefore(command, arg, collected)) : arg.choices ?? [];
150
162
  setPending({ command, arg, collected });
151
163
  setQuery('');
152
164
  /*
@@ -253,8 +265,10 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
253
265
  // anybody chose to be at: the caller drilled in on their behalf, and
254
266
  // the level underneath is a list of one. Backing out to it reads as
255
267
  // "escape did nothing", and the second escape - the one that would
256
- // close it - is spent leaving a screen nobody asked to see.
257
- if (pending && !openAt) {
268
+ // close it - is spent leaving a screen nobody asked to see. A question
269
+ // that came after one is different: there the level underneath is the
270
+ // earlier question, which somebody did choose to be at.
271
+ if (pending && (!openAt || previousOf(pending) !== undefined)) {
258
272
  back();
259
273
  return true;
260
274
  }
@@ -267,7 +281,7 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
267
281
  onClose?.();
268
282
  return true;
269
283
  }
270
- if (event.name === 'left' && pending && query === '' && !openAt) {
284
+ if (event.name === 'left' && pending && query === '' && (!openAt || previousOf(pending) !== undefined)) {
271
285
  back();
272
286
  return true;
273
287
  }
@@ -297,6 +311,8 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
297
311
  // there - and a sentence about what a mode does is exactly the thing a
298
312
  // person needs whole. So the line under the list follows the highlight: the
299
313
  // choice's description while there is one, the question's otherwise.
314
+ // It is the one line in the panel that wraps rather than truncates,
315
+ // because it is the one place a sentence can be read whole.
300
316
  const chosen = pending ? offered[index] : undefined;
301
317
  const detail = pending
302
318
  ? chosen?.description
@@ -315,7 +331,14 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
315
331
  * the things that decide it: a question with two short answers still needs
316
332
  * somewhere to type and a line saying what the keys do.
317
333
  */
318
- const content = Math.max(...items.map((item) => rowWidth(item, descriptions)), ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]));
334
+ const content = Math.max(...items.map((item) => rowWidth(item, descriptions)), ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]),
335
+ // The sentences the line under the list may show, so a question with
336
+ // three one-word answers is not a panel 28 wide wrapping a sentence
337
+ // three times under them. Capped like everything else by `maxWidth`.
338
+ ...(pending
339
+ ? [pending.arg.description ?? '', ...offered.map((choice) => choice.description ?? '')]
340
+ .map((sentence) => (sentence === '' ? 0 : stringWidth(sentence) + 6))
341
+ : []));
319
342
  return h('box', {
320
343
  role: 'dialog',
321
344
  label: 'Commands',
@@ -361,10 +384,19 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
361
384
  if (command && argumentOf(command))
362
385
  choose();
363
386
  },
387
+ // A typed answer's field is the answer, so its placeholder is the
388
+ // question. A list's field is a filter, and the question goes where
389
+ // there is room to say it once: here, while the line under the list
390
+ // is busy saying what each answer means; under the list, whole and
391
+ // wrapped, when the answers have nothing to say for themselves and
392
+ // that line would otherwise repeat this one, cut to whatever width
393
+ // three short answers gave the panel.
364
394
  placeholder: pending
365
- ? (pending.arg.description ?? (pending.arg.choices === undefined
366
- ? `${pending.command.title}${theme.glyphs.ellipsis}`
367
- : `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`))
395
+ ? (pending.arg.choices === undefined
396
+ ? pending.arg.description ?? `${pending.command.title}${theme.glyphs.ellipsis}`
397
+ : offered.some((choice) => choice.description) && pending.arg.description
398
+ ? pending.arg.description
399
+ : `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`)
368
400
  : (placeholder ?? `Type a command${theme.glyphs.ellipsis}`),
369
401
  search: true,
370
402
  autoFocus: true,
@@ -386,7 +418,7 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
386
418
  }), h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }),
387
419
  // What the highlighted row actually is, and how to move around. A palette
388
420
  // that shows only titles makes you run something to find out what it does.
389
- h('box', { direction: 'row', gap: 1 }, h('text', { content: detail, fg: 'muted', flex: 1, truncate: 'end' }), h('text', { content: `${rows.length}`, fg: 'subtle' })), h('box', { direction: 'row', gap: 1 }, h('text', {
421
+ h('box', { direction: 'row', gap: 1, align: 'start' }, h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }), h('text', { content: `${rows.length}`, fg: 'subtle' })), h('box', { direction: 'row', gap: 1 }, h('text', {
390
422
  content: pending
391
423
  ? (pending.arg.choices === undefined
392
424
  ? hint(theme, ['type it', 'enter confirm', 'esc back'])
@@ -418,6 +450,23 @@ export function argumentOf(command, collected = {}) {
418
450
  return (command.args ?? []).find((arg) => collected[arg.name] === undefined &&
419
451
  (arg.choices !== undefined || (arg.required === true && arg.default === undefined)));
420
452
  }
453
+ /**
454
+ * What a `choices` resolver is told: the answers given, under the defaults
455
+ * of the arguments before this one that stood in for a question.
456
+ *
457
+ * Only the ones ahead of it in the declaration. An argument after this one
458
+ * has not been reached, and its default is not yet an answer to anything.
459
+ */
460
+ function answeredBefore(command, arg, collected) {
461
+ const answered = {};
462
+ for (const spec of command.args ?? []) {
463
+ if (spec === arg)
464
+ break;
465
+ if (spec.default !== undefined)
466
+ answered[spec.name] = spec.default;
467
+ }
468
+ return { ...answered, ...collected };
469
+ }
421
470
  /** The short form and the long one, as one shape. */
422
471
  function asChoice(choice) {
423
472
  return typeof choice === 'string' ? { value: choice } : choice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textui/widgets",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "The TextUI component catalog - layout, display, controls, data, overlays and charts",
5
5
  "keywords": [
6
6
  "terminal",
@@ -46,7 +46,7 @@
46
46
  "test": "vitest run"
47
47
  },
48
48
  "dependencies": {
49
- "@textui/core": "^0.5.0"
49
+ "@textui/core": "^0.6.1"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"
@@ -89,6 +89,15 @@ export interface CommandPaletteProps extends BoxProps {
89
89
  * That is where sub-items come from - the command says what it needs and the
90
90
  * palette asks, rather than every caller inventing its own submenu.
91
91
  */
92
+
93
+ /** The question being asked: the command, the argument, and the answers given before it. */
94
+ interface Pending {
95
+ command: CommandDefinition;
96
+ arg: ArgSpec;
97
+ /** Answers given so far, for a command that asks for more than one. */
98
+ collected: Record<string, unknown>;
99
+ }
100
+
92
101
  export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalette', (props) => {
93
102
  const theme = useTheme();
94
103
  const runtime = useRuntime();
@@ -101,12 +110,7 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
101
110
  const [query, setQuery] = useState('');
102
111
  const [highlight, setHighlight] = useState(0);
103
112
  /** The command being asked about, when the palette has drilled in. */
104
- const [pending, setPending] = useState<{
105
- command: CommandDefinition;
106
- arg: ArgSpec;
107
- /** Answers given so far, for a command that asks for more than one. */
108
- collected: Record<string, unknown>;
109
- } | null>(null);
113
+ const [pending, setPending] = useState<Pending | null>(null);
110
114
  const [choices, setChoices] = useState<ArgChoice[]>([]);
111
115
  /**
112
116
  * Whether the answer to "what may I choose" is still on its way.
@@ -186,10 +190,30 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
186
190
  : {}),
187
191
  }));
188
192
 
193
+ /** The argument answered last, when this question came after one. */
194
+ const previousOf = (asked: Pending): ArgSpec | undefined => {
195
+ const last = Object.keys(asked.collected).at(-1);
196
+ return last === undefined ? undefined : (asked.command.args ?? []).find((arg) => arg.name === last);
197
+ };
198
+
199
+ /**
200
+ * One question back.
201
+ *
202
+ * After a first question the second is asked again from the start with
203
+ * the earlier answer withdrawn: a wrong provider is corrected by choosing
204
+ * another, not by closing the whole picker and opening it again. At the
205
+ * first question, the command list.
206
+ */
189
207
  const back = (): void => {
190
208
  // `null` is "never mind": whatever the preview did gets undone by whoever
191
209
  // did it, because only the command knows what it changed.
192
210
  pending?.arg.preview?.(null);
211
+ const previous = pending ? previousOf(pending) : undefined;
212
+ if (pending && previous) {
213
+ const { [previous.name]: _withdrawn, ...rest } = pending.collected;
214
+ drillInto(pending.command, previous, rest);
215
+ return;
216
+ }
193
217
  setPending(null);
194
218
  setChoices([]);
195
219
  setQuery('');
@@ -233,13 +257,20 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
233
257
  onRun?.(id, args);
234
258
  };
235
259
 
236
- /** Ask about one command. `choices` may be a function, and may be async. */
260
+ /**
261
+ * Ask about one command. `choices` may be a function, and may be async.
262
+ *
263
+ * A resolver is handed what was answered before this argument - and an
264
+ * argument that was not asked because its `default` stood was answered by
265
+ * that default. Left out, "the models of the chosen provider" would see no
266
+ * provider exactly when there was only one to choose from.
267
+ */
237
268
  const drillInto = (
238
269
  command: CommandDefinition,
239
270
  arg: ArgSpec,
240
271
  collected: Record<string, unknown> = {},
241
272
  ): void => {
242
- const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
273
+ const resolved = typeof arg.choices === 'function' ? arg.choices(answeredBefore(command, arg, collected)) : arg.choices ?? [];
243
274
  setPending({ command, arg, collected });
244
275
  setQuery('');
245
276
 
@@ -350,8 +381,10 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
350
381
  // anybody chose to be at: the caller drilled in on their behalf, and
351
382
  // the level underneath is a list of one. Backing out to it reads as
352
383
  // "escape did nothing", and the second escape - the one that would
353
- // close it - is spent leaving a screen nobody asked to see.
354
- if (pending && !openAt) { back(); return true; }
384
+ // close it - is spent leaving a screen nobody asked to see. A question
385
+ // that came after one is different: there the level underneath is the
386
+ // earlier question, which somebody did choose to be at.
387
+ if (pending && (!openAt || previousOf(pending) !== undefined)) { back(); return true; }
355
388
  // Whatever a highlighted choice previewed has to be put back on the
356
389
  // way out, and `null` is how the command is told to undo it - only it
357
390
  // knows what it changed. `back()` did that, so closing instead of
@@ -361,7 +394,7 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
361
394
  onClose?.();
362
395
  return true;
363
396
  }
364
- if (event.name === 'left' && pending && query === '' && !openAt) { back(); return true; }
397
+ if (event.name === 'left' && pending && query === '' && (!openAt || previousOf(pending) !== undefined)) { back(); return true; }
365
398
  // Wrapping, both ways. A list you can walk off the end of makes you
366
399
  // check where you are before every press; one that comes round means the
367
400
  // last item is one key from the first.
@@ -389,6 +422,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
389
422
  // there - and a sentence about what a mode does is exactly the thing a
390
423
  // person needs whole. So the line under the list follows the highlight: the
391
424
  // choice's description while there is one, the question's otherwise.
425
+ // It is the one line in the panel that wraps rather than truncates,
426
+ // because it is the one place a sentence can be read whole.
392
427
  const chosen = pending ? offered[index] : undefined;
393
428
  const detail = pending
394
429
  ? chosen?.description
@@ -411,6 +446,13 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
411
446
  const content = Math.max(
412
447
  ...items.map((item) => rowWidth(item, descriptions)),
413
448
  ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]),
449
+ // The sentences the line under the list may show, so a question with
450
+ // three one-word answers is not a panel 28 wide wrapping a sentence
451
+ // three times under them. Capped like everything else by `maxWidth`.
452
+ ...(pending
453
+ ? [pending.arg.description ?? '', ...offered.map((choice) => choice.description ?? '')]
454
+ .map((sentence) => (sentence === '' ? 0 : stringWidth(sentence) + 6))
455
+ : []),
414
456
  );
415
457
 
416
458
  return h('box', {
@@ -457,10 +499,19 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
457
499
  const command = matches[index];
458
500
  if (command && argumentOf(command)) choose();
459
501
  },
502
+ // A typed answer's field is the answer, so its placeholder is the
503
+ // question. A list's field is a filter, and the question goes where
504
+ // there is room to say it once: here, while the line under the list
505
+ // is busy saying what each answer means; under the list, whole and
506
+ // wrapped, when the answers have nothing to say for themselves and
507
+ // that line would otherwise repeat this one, cut to whatever width
508
+ // three short answers gave the panel.
460
509
  placeholder: pending
461
- ? (pending.arg.description ?? (pending.arg.choices === undefined
462
- ? `${pending.command.title}${theme.glyphs.ellipsis}`
463
- : `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`))
510
+ ? (pending.arg.choices === undefined
511
+ ? pending.arg.description ?? `${pending.command.title}${theme.glyphs.ellipsis}`
512
+ : offered.some((choice) => choice.description) && pending.arg.description
513
+ ? pending.arg.description
514
+ : `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`)
464
515
  : (placeholder ?? `Type a command${theme.glyphs.ellipsis}`),
465
516
  search: true,
466
517
  autoFocus: true,
@@ -484,8 +535,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
484
535
  h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }),
485
536
  // What the highlighted row actually is, and how to move around. A palette
486
537
  // that shows only titles makes you run something to find out what it does.
487
- h('box', { direction: 'row', gap: 1 },
488
- h('text', { content: detail, fg: 'muted', flex: 1, truncate: 'end' }),
538
+ h('box', { direction: 'row', gap: 1, align: 'start' },
539
+ h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }),
489
540
  h('text', { content: `${rows.length}`, fg: 'subtle' })),
490
541
  h('box', { direction: 'row', gap: 1 },
491
542
  h('text', {
@@ -528,6 +579,26 @@ export function argumentOf(
528
579
  );
529
580
  }
530
581
 
582
+ /**
583
+ * What a `choices` resolver is told: the answers given, under the defaults
584
+ * of the arguments before this one that stood in for a question.
585
+ *
586
+ * Only the ones ahead of it in the declaration. An argument after this one
587
+ * has not been reached, and its default is not yet an answer to anything.
588
+ */
589
+ function answeredBefore(
590
+ command: CommandDefinition,
591
+ arg: ArgSpec,
592
+ collected: Record<string, unknown>,
593
+ ): Record<string, unknown> {
594
+ const answered: Record<string, unknown> = {};
595
+ for (const spec of command.args ?? []) {
596
+ if (spec === arg) break;
597
+ if (spec.default !== undefined) answered[spec.name] = spec.default;
598
+ }
599
+ return { ...answered, ...collected };
600
+ }
601
+
531
602
  /** The short form and the long one, as one shape. */
532
603
  function asChoice(choice: string | ArgChoice): ArgChoice {
533
604
  return typeof choice === 'string' ? { value: choice } : choice;