killeros 1.5.8 → 2.0.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.
@@ -1,19 +1,14 @@
1
- import { type ExtensionAPI, type ThemeColor } from "@earendil-works/pi-coding-agent";
1
+ import { keyHint, type ExtensionAPI, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
- Container,
4
3
  decodeKittyPrintable,
5
4
  Editor,
6
- Key,
7
5
  Markdown,
8
- matchesKey,
9
- SelectList,
10
- Text,
11
6
  truncateToWidth,
12
- visibleWidth,
13
7
  wrapTextWithAnsi,
14
8
  type EditorTheme,
15
9
  } from "@earendil-works/pi-tui";
16
10
  import { Type } from "typebox";
11
+ import { BoundedText } from "./bounded-text.ts";
17
12
 
18
13
  const OptionSchema = Type.Object({
19
14
  label: Type.String({ minLength: 1, maxLength: 200, description: "Display label for the option" }),
@@ -90,6 +85,16 @@ function removeLastGrapheme(value: string): string {
90
85
  return last ? value.slice(0, last.index) : "";
91
86
  }
92
87
 
88
+ function oneLine(value: string): string {
89
+ return value.replace(/\s+/gu, " ").trim();
90
+ }
91
+
92
+ function visibleOptionRange(total: number, selected: number, capacity: number): { start: number; end: number } {
93
+ const size = Math.max(1, Math.min(total, capacity));
94
+ const start = Math.max(0, Math.min(selected - Math.floor(size / 2), total - size));
95
+ return { start, end: Math.min(total, start + size) };
96
+ }
97
+
93
98
  export function registerQuestionTool(pi: ExtensionAPI): void {
94
99
  const customInputHistory: string[] = [];
95
100
  let customInputHistoryBytes = 0;
@@ -153,11 +158,12 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
153
158
  ];
154
159
 
155
160
  let finishFromAbort: (() => void) | undefined;
156
- const resultPromise = ctx.ui.custom<QuestionSelection>((tui, theme, _keybindings, done) => {
161
+ const resultPromise = ctx.ui.custom<QuestionSelection>((tui, theme, keybindings, done) => {
157
162
  let optionIndex = 0;
158
163
  let editMode = false;
159
164
  let filterQuery = "";
160
165
  let cachedWidth: number | undefined;
166
+ let cachedRows: number | undefined;
161
167
  let cachedLines: string[] | undefined;
162
168
  let completed = false;
163
169
 
@@ -191,6 +197,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
191
197
 
192
198
  const invalidate = (): void => {
193
199
  cachedWidth = undefined;
200
+ cachedRows = undefined;
194
201
  cachedLines = undefined;
195
202
  editor.invalidate();
196
203
  };
@@ -241,7 +248,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
241
248
 
242
249
  const handleInput = (data: string): void => {
243
250
  if (editMode) {
244
- if (matchesKey(data, Key.escape)) {
251
+ if (keybindings.matches(data, "tui.select.cancel")) {
245
252
  editMode = false;
246
253
  editor.setText("");
247
254
  refresh();
@@ -260,24 +267,35 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
260
267
 
261
268
  const visibleOptions = filteredOptions();
262
269
  if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
263
- if (matchesKey(data, Key.up)) {
270
+ const pageSize = Math.max(1, Math.min(5, Math.ceil(Math.max(1, tui.terminal.rows - 5) / 2)));
271
+ if (keybindings.matches(data, "tui.select.up")) {
264
272
  optionIndex = Math.max(0, optionIndex - 1);
265
273
  refresh();
266
274
  return;
267
275
  }
268
- if (matchesKey(data, Key.down)) {
276
+ if (keybindings.matches(data, "tui.select.down")) {
269
277
  optionIndex = Math.min(visibleOptions.length - 1, optionIndex + 1);
270
278
  refresh();
271
279
  return;
272
280
  }
273
- if (matchesKey(data, Key.enter)) {
281
+ if (keybindings.matches(data, "tui.select.pageUp")) {
282
+ optionIndex = Math.max(0, optionIndex - pageSize);
283
+ refresh();
284
+ return;
285
+ }
286
+ if (keybindings.matches(data, "tui.select.pageDown")) {
287
+ optionIndex = Math.min(visibleOptions.length - 1, optionIndex + pageSize);
288
+ refresh();
289
+ return;
290
+ }
291
+ if (keybindings.matches(data, "tui.select.confirm")) {
274
292
  const selected = visibleOptions[optionIndex];
275
293
  if (!selected) return;
276
294
  if (selected.isOther) enterCustomMode();
277
295
  else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
278
296
  return;
279
297
  }
280
- if (matchesKey(data, Key.escape)) {
298
+ if (keybindings.matches(data, "tui.select.cancel")) {
281
299
  if (filterQuery) {
282
300
  filterQuery = "";
283
301
  optionIndex = 0;
@@ -287,7 +305,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
287
305
  }
288
306
  return;
289
307
  }
290
- if (matchesKey(data, Key.backspace)) {
308
+ if (keybindings.matches(data, "tui.editor.deleteCharBackward")) {
291
309
  if (filterQuery) {
292
310
  filterQuery = removeLastGrapheme(filterQuery);
293
311
  optionIndex = 0;
@@ -304,60 +322,94 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
304
322
  else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
305
323
  return;
306
324
  }
307
- if (printableInput) {
308
- appendFilterInput(printableInput);
309
- }
325
+ if (printableInput) appendFilterInput(printableInput);
310
326
  };
311
327
 
312
328
  const render = (width: number): string[] => {
313
- const renderWidth = Math.max(1, width);
314
- if (cachedLines && cachedWidth === renderWidth) return cachedLines;
315
- const lines: string[] = [];
316
- const addWrapped = (text: string): void => {
317
- lines.push(...wrapTextWithAnsi(text, renderWidth));
318
- };
319
- const addWrappedWithPrefix = (prefix: string, text: string): void => {
320
- const prefixWidth = visibleWidth(prefix);
321
- if (prefixWidth >= renderWidth) {
322
- addWrapped(prefix + text);
323
- return;
324
- }
325
- const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
326
- const continuation = " ".repeat(prefixWidth);
327
- wrapped.forEach((line, index) => lines.push(`${index === 0 ? prefix : continuation}${line}`));
328
- };
329
+ if (width <= 0) return [];
330
+ const renderWidth = width;
331
+ const rowBudget = Math.max(1, tui.terminal.rows);
332
+ if (cachedLines && cachedWidth === renderWidth && cachedRows === rowBudget) return cachedLines;
333
+ const visibleOptions = filteredOptions();
334
+ if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
335
+ const selected = visibleOptions[optionIndex];
336
+ const position = `Option ${Math.min(optionIndex + 1, visibleOptions.length)}/${visibleOptions.length}`;
337
+ const answerCount = inputCharacterCount(editor.getExpandedText()).toLocaleString();
338
+ const filterCount = inputCharacterCount(filterQuery).toLocaleString();
339
+
340
+ if (rowBudget <= 2) {
341
+ const compact = editMode
342
+ ? [`Answer ${answerCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`, editor.getExpandedText() || "Type an answer"]
343
+ : [`${selected ? `> ${selected.label}` : "No matching options"} · ${position}`];
344
+ cachedWidth = renderWidth;
345
+ cachedRows = rowBudget;
346
+ cachedLines = compact.slice(0, rowBudget).map((line) => truncateToWidth(line, renderWidth, "…"));
347
+ return cachedLines;
348
+ }
329
349
 
330
- lines.push(theme.fg("accent", "─".repeat(renderWidth)));
331
- addWrappedWithPrefix(" ", theme.fg("text", params.question));
332
- lines.push("");
333
- if (!editMode && filterQuery) {
334
- addWrappedWithPrefix(" ", `${theme.fg("muted", "Filter: ")}${theme.fg("accent", filterQuery)}`);
335
- lines.push("");
350
+ if (rowBudget <= 5) {
351
+ const compact = [
352
+ truncateToWidth(params.question.replace(/\s+/gu, " "), renderWidth, "…"),
353
+ editMode
354
+ ? `Answer ${answerCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`
355
+ : `${selected ? `> ${selected.label}` : "No matching options"} · ${position}`,
356
+ editMode
357
+ ? editor.getExpandedText() || "Type an answer"
358
+ : filterQuery
359
+ ? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()}`
360
+ : position,
361
+ editMode
362
+ ? `${keyHint("tui.input.submit", "submit")} • ${keyHint("tui.select.cancel", "options")}`
363
+ : `${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`,
364
+ ];
365
+ cachedWidth = renderWidth;
366
+ cachedRows = rowBudget;
367
+ cachedLines = compact.slice(0, rowBudget).map((line) => truncateToWidth(line, renderWidth, "…"));
368
+ return cachedLines;
336
369
  }
337
370
 
338
- const visibleOptions = filteredOptions();
339
- if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
340
- visibleOptions.forEach((option, index) => {
341
- const selected = index === optionIndex;
342
- const prefix = selected ? theme.fg("accent", "> ") : " ";
343
- const color: ThemeColor = selected ? "accent" : "text";
344
- addWrappedWithPrefix(prefix, theme.fg(color, `${index + 1}. ${option.label}`));
345
- if (selected && option.description) {
346
- addWrappedWithPrefix(" ", theme.fg("muted", option.description));
371
+ const contentRows = rowBudget - 5;
372
+ const optionCapacity = Math.max(1, Math.min(5, Math.ceil(contentRows / 2)));
373
+ const detailCapacity = Math.max(0, contentRows - optionCapacity);
374
+ const { start, end } = visibleOptionRange(visibleOptions.length, optionIndex, optionCapacity);
375
+ const hiddenAbove = start > 0 ? `↑ ${start}` : "";
376
+ const hiddenBelowCount = visibleOptions.length - end;
377
+ const hiddenBelow = hiddenBelowCount > 0 ? `↓ ${hiddenBelowCount}` : "";
378
+ const hiddenStatus = [hiddenAbove, hiddenBelow].filter(Boolean).join(" · ");
379
+ const progress = editMode
380
+ ? `Answer ${answerCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`
381
+ : filterQuery
382
+ ? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} · ${position}`
383
+ : `${position}${hiddenStatus ? ` · ${hiddenStatus}` : ""}`;
384
+ const hint = editMode
385
+ ? `${keyHint("tui.input.submit", "submit")} • ${keyHint("tui.select.cancel", "options")}`
386
+ : `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", filterQuery ? "clear filter" : "cancel")}`;
387
+ const lines: string[] = [
388
+ theme.fg("accent", "─".repeat(renderWidth)),
389
+ theme.fg("text", truncateToWidth(` ${params.question.replace(/\s+/gu, " ")}`, renderWidth, "…")),
390
+ theme.fg("muted", truncateToWidth(` ${progress}`, renderWidth, "…")),
391
+ ];
392
+
393
+ if (editMode) {
394
+ const editorLines = editor.render(renderWidth);
395
+ const draftLines = editorLines.length > 2 ? editorLines.slice(1, -1) : editorLines;
396
+ lines.push(...(draftLines.length > 0 ? draftLines : ["Type an answer"]).slice(-contentRows));
397
+ } else {
398
+ for (let index = start; index < end; index += 1) {
399
+ const option = visibleOptions[index]!;
400
+ const isSelected = index === optionIndex;
401
+ const prefix = isSelected ? "> " : " ";
402
+ const color: ThemeColor = isSelected ? "accent" : "text";
403
+ lines.push(theme.fg(color, truncateToWidth(`${prefix}${index + 1}. ${option.label}`, renderWidth, "…")));
347
404
  }
348
- });
349
-
350
- const selectedPreview = visibleOptions[optionIndex]?.preview;
351
- if (!editMode && selectedPreview) {
352
- const footerRows = 3;
353
- const previewChromeRows = 2;
354
- const availableRows = tui.terminal.rows - lines.length - footerRows;
355
- if (availableRows > previewChromeRows) {
356
- lines.push("");
357
- addWrappedWithPrefix(" ", theme.fg("accent", theme.bold("Proposal preview")));
358
- const markdownLines = new Markdown(
359
- selectedPreview,
360
- 1,
405
+
406
+ const detailLines: string[] = [];
407
+ if (selected?.description) detailLines.push(...wrapTextWithAnsi(theme.fg("muted", selected.description), renderWidth));
408
+ if (selected?.preview) {
409
+ detailLines.push(theme.fg("accent", theme.bold("Proposal preview")));
410
+ detailLines.push(...new Markdown(
411
+ selected.preview,
412
+ 0,
361
413
  0,
362
414
  {
363
415
  heading: (text) => theme.fg("accent", theme.bold(text)),
@@ -376,35 +428,23 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
376
428
  underline: (text) => theme.underline(text),
377
429
  },
378
430
  { color: (text) => theme.fg("muted", text) },
379
- ).render(renderWidth);
380
- const maxPreviewRows = Math.min(12, availableRows - previewChromeRows);
381
- if (markdownLines.length <= maxPreviewRows) {
382
- lines.push(...markdownLines);
383
- } else {
384
- const visiblePreviewRows = Math.max(0, maxPreviewRows - 1);
385
- lines.push(...markdownLines.slice(0, visiblePreviewRows));
386
- const hiddenRows = markdownLines.length - visiblePreviewRows;
387
- lines.push(theme.fg("dim", ` … ${hiddenRows} more line${hiddenRows === 1 ? "" : "s"}`));
388
- }
431
+ ).render(renderWidth));
432
+ }
433
+ if (detailCapacity > 0 && detailLines.length > detailCapacity) {
434
+ const visibleDetailRows = Math.max(0, detailCapacity - 1);
435
+ lines.push(...detailLines.slice(0, visibleDetailRows));
436
+ const hiddenRows = detailLines.length - visibleDetailRows;
437
+ lines.push(theme.fg("dim", `… ${hiddenRows} more line${hiddenRows === 1 ? "" : "s"}`));
438
+ } else {
439
+ lines.push(...detailLines.slice(0, detailCapacity));
389
440
  }
390
441
  }
391
442
 
392
- if (editMode) {
393
- lines.push("");
394
- addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:"));
395
- editor.render(Math.max(1, renderWidth - 2)).forEach((line) => lines.push(` ${line}`));
396
- }
397
-
398
- lines.push("");
399
- const hint = editMode
400
- ? `Enter submit • Esc options${customInputHistory.length ? " • ↑↓ history" : ""}`
401
- : filterQuery
402
- ? "1-9 select • ↑↓ navigate • Enter select • Esc clear filter"
403
- : "1-9 select • type to filter • ↑↓ navigate • Enter select • Esc cancel";
404
- addWrappedWithPrefix(" ", theme.fg("dim", hint));
443
+ lines.push(theme.fg("dim", truncateToWidth(` ${hint}`, renderWidth, "…")));
405
444
  lines.push(theme.fg("accent", "─".repeat(renderWidth)));
406
445
  cachedWidth = renderWidth;
407
- cachedLines = lines.map((line) => truncateToWidth(line, renderWidth, ""));
446
+ cachedRows = rowBudget;
447
+ cachedLines = lines.slice(0, rowBudget).map((line) => truncateToWidth(line, renderWidth, ""));
408
448
  return cachedLines;
409
449
  };
410
450
 
@@ -457,27 +497,34 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
457
497
  };
458
498
  },
459
499
 
460
- renderCall(args, theme) {
461
- let text = `${theme.fg("toolTitle", theme.bold("question "))}${theme.fg("muted", args.question)}`;
462
- if (args.options.length) {
463
- const numbered = [...args.options.map((option) => option.label), "Type a custom answer"]
464
- .map((option, index) => `${index + 1}. ${option}`);
465
- text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
500
+ renderCall(args, theme, context) {
501
+ if (!context.expanded) {
502
+ const summary = `${theme.fg("toolTitle", theme.bold("question "))}${theme.fg("muted", oneLine(args.question))}\n${theme.fg("dim", ` ${args.options.length} option${args.options.length === 1 ? "" : "s"}`)}`;
503
+ return new BoundedText(summary, 3);
466
504
  }
467
- return new Text(text, 0, 0);
505
+
506
+ const lines = [`${theme.fg("toolTitle", theme.bold("question "))}${theme.fg("muted", args.question)}`];
507
+ args.options.forEach((option, index) => {
508
+ lines.push(theme.fg("text", `${index + 1}. ${option.label}`));
509
+ if (option.description) lines.push(theme.fg("muted", ` ${option.description}`));
510
+ if (option.preview) lines.push(theme.fg("dim", option.preview));
511
+ });
512
+ lines.push(theme.fg("text", `${args.options.length + 1}. Type a custom answer`));
513
+ return new BoundedText(lines.join("\n"));
468
514
  },
469
515
 
470
- renderResult(result, _options, theme) {
516
+ renderResult(result, options, theme) {
471
517
  const details = result.details;
472
518
  if (!details) {
473
519
  const first = result.content[0];
474
- return new Text(first?.type === "text" ? first.text : "", 0, 0);
520
+ return new BoundedText(first?.type === "text" ? first.text : "", options.expanded ? undefined : 3);
475
521
  }
476
- if (details.cancelled || details.answer === null) return new Text(theme.fg("warning", "Cancelled"), 0, 0);
522
+ if (details.cancelled || details.answer === null) return new BoundedText(theme.fg("warning", "Cancelled"));
477
523
  if (details.wasCustom) {
478
- return new Text(`${theme.fg("success", "✓ ")}${theme.fg("muted", "(wrote) ")}${theme.fg("accent", details.answer)}`, 0, 0);
524
+ const text = `${theme.fg("success", "✓ ")}${theme.fg("muted", "(wrote) ")}${theme.fg("accent", details.answer)}`;
525
+ return new BoundedText(text, options.expanded ? undefined : 3);
479
526
  }
480
- return new Text(`${theme.fg("success", "✓ ")}${theme.fg("accent", details.answer)}`, 0, 0);
527
+ return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("accent", details.answer)}`);
481
528
  },
482
529
  });
483
530
  }