killeros 2.0.20 → 2.0.21
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.
- package/CHANGELOG.md +17 -0
- package/README.md +2 -2
- package/killeros/display.ts +8 -3
- package/killeros/footer.ts +118 -25
- package/killeros/goal-state.ts +407 -0
- package/killeros/goals.ts +21 -375
- package/killeros/handoff.ts +39 -16
- package/killeros/init-evidence.ts +15 -3
- package/killeros/personal-instructions.ts +57 -5
- package/killeros/question-ui.ts +590 -0
- package/killeros/question.ts +12 -558
- package/killeros/secret-detector.ts +19 -0
- package/killeros/shell-ui.ts +1 -1
- package/package.json +5 -5
package/killeros/question.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
3
|
type ExtensionAPI,
|
|
4
|
-
type ThemeColor,
|
|
5
4
|
type ToolDefinition,
|
|
6
5
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
8
|
-
decodeKittyPrintable,
|
|
9
|
-
Editor,
|
|
10
|
-
Markdown,
|
|
11
|
-
truncateToWidth,
|
|
12
|
-
visibleWidth,
|
|
13
|
-
wrapTextWithAnsi,
|
|
14
|
-
type EditorTheme,
|
|
15
|
-
} from "@earendil-works/pi-tui";
|
|
16
6
|
import { Type, type Static } from "typebox";
|
|
17
7
|
import { BoundedText } from "./bounded-text.ts";
|
|
8
|
+
import { CUSTOM_INPUT_HISTORY_BYTES, CUSTOM_INPUT_HISTORY_LIMIT, FILTER_QUERY_MAX_BYTES, FILTER_QUERY_MAX_CHARACTERS, MultipleResultText, oneLine, openQuestionUi, type DisplayOption } from "./question-ui.ts";
|
|
18
9
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
19
10
|
|
|
20
11
|
const OptionSchema = Type.Object({
|
|
@@ -73,14 +64,6 @@ function normalizeQuestionSelection(params: QuestionParamsValue): NormalizedQues
|
|
|
73
64
|
return { mode, minSelections, maxSelections };
|
|
74
65
|
}
|
|
75
66
|
|
|
76
|
-
interface DisplayOption {
|
|
77
|
-
label: string;
|
|
78
|
-
description?: string;
|
|
79
|
-
preview?: string;
|
|
80
|
-
originalIndex: number;
|
|
81
|
-
isOther: boolean;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
67
|
interface SingleQuestionDetails {
|
|
85
68
|
question: string;
|
|
86
69
|
options: string[];
|
|
@@ -102,121 +85,6 @@ interface MultipleQuestionDetails {
|
|
|
102
85
|
|
|
103
86
|
type QuestionDetails = SingleQuestionDetails | MultipleQuestionDetails;
|
|
104
87
|
|
|
105
|
-
type QuestionSelection =
|
|
106
|
-
| { kind: "selected"; answer: string; originalIndex: number }
|
|
107
|
-
| { kind: "custom"; answer: string }
|
|
108
|
-
| { kind: "multiple"; answers: string[]; selectedIndices: number[]; customAnswer?: string }
|
|
109
|
-
| { kind: "cancelled" }
|
|
110
|
-
| { kind: "aborted" };
|
|
111
|
-
|
|
112
|
-
const CUSTOM_INPUT_MAX_CHARACTERS = 4_000;
|
|
113
|
-
const CUSTOM_INPUT_HISTORY_LIMIT = 100;
|
|
114
|
-
const CUSTOM_INPUT_HISTORY_BYTES = 64 * 1024;
|
|
115
|
-
const FILTER_QUERY_MAX_CHARACTERS = 4_000;
|
|
116
|
-
const FILTER_QUERY_MAX_BYTES = 16_000;
|
|
117
|
-
|
|
118
|
-
function isPrintableInput(data: string): boolean {
|
|
119
|
-
return data.length > 0 && !/[\u0000-\u001F\u007F-\u009F]/u.test(data);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
123
|
-
|
|
124
|
-
function decodeQuestionFilterInput(data: string): string | undefined {
|
|
125
|
-
const kittyPrintable = decodeKittyPrintable(data);
|
|
126
|
-
if (kittyPrintable !== undefined) return isPrintableInput(kittyPrintable) ? kittyPrintable : undefined;
|
|
127
|
-
|
|
128
|
-
const pasteStart = "\x1B[200~";
|
|
129
|
-
const pasteEnd = "\x1B[201~";
|
|
130
|
-
const startIndex = data.indexOf(pasteStart);
|
|
131
|
-
const endIndex = data.indexOf(pasteEnd, startIndex + pasteStart.length);
|
|
132
|
-
if (startIndex >= 0 && endIndex >= 0) {
|
|
133
|
-
return data
|
|
134
|
-
.slice(startIndex + pasteStart.length, endIndex)
|
|
135
|
-
.replace(/\r\n|\r|\n/gu, "")
|
|
136
|
-
.replace(/\t/gu, " ")
|
|
137
|
-
.replace(/[\u0000-\u001F\u007F-\u009F]/gu, "");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return isPrintableInput(data) ? data : undefined;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function removeLastGrapheme(value: string): string {
|
|
144
|
-
const segments = [...graphemeSegmenter.segment(value)];
|
|
145
|
-
const last = segments.at(-1);
|
|
146
|
-
return last ? value.slice(0, last.index) : "";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function oneLine(value: string): string {
|
|
150
|
-
return value.replace(/\s+/gu, " ").trim();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function boundedRenderLine(value: string, width: number, suffix: string): string {
|
|
154
|
-
return truncateToWidth(value.replace(/\r\n|\r|\n/gu, " "), width, suffix);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function visibleOptionRange(total: number, selected: number, capacity: number): { start: number; end: number } {
|
|
158
|
-
const size = Math.max(1, Math.min(total, capacity));
|
|
159
|
-
const start = Math.max(0, Math.min(selected - Math.floor(size / 2), total - size));
|
|
160
|
-
return { start, end: Math.min(total, start + size) };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function boundedQuestionLines(question: string, width: number, rowLimit: number): string[] {
|
|
164
|
-
const wrapped = wrapTextWithAnsi(question.replace(/\s+/gu, " ").trim(), width);
|
|
165
|
-
if (wrapped.length <= rowLimit) return wrapped;
|
|
166
|
-
const visible = wrapped.slice(0, rowLimit);
|
|
167
|
-
const finalIndex = rowLimit - 1;
|
|
168
|
-
const finalLine = visible[finalIndex];
|
|
169
|
-
if (finalLine !== undefined) visible[finalIndex] = truncateToWidth(finalLine, width, "…");
|
|
170
|
-
return visible;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function compactMultipleAnswers(answers: readonly string[], width: number): string {
|
|
174
|
-
const prefix = "✓ ";
|
|
175
|
-
if (answers.length === 0) return truncateToWidth(prefix + "No answers", width, "…");
|
|
176
|
-
const visible: string[] = [];
|
|
177
|
-
for (const [index, answer] of answers.entries()) {
|
|
178
|
-
const remaining = answers.length - index - 1;
|
|
179
|
-
const candidate = [...visible, oneLine(answer)].join(", ");
|
|
180
|
-
const suffix = remaining > 0 ? `, +${remaining} more` : "";
|
|
181
|
-
if (visibleWidth(prefix + candidate + suffix) > width) break;
|
|
182
|
-
visible.push(oneLine(answer));
|
|
183
|
-
}
|
|
184
|
-
if (visible.length === answers.length) return prefix + visible.join(", ");
|
|
185
|
-
const hidden = answers.length - visible.length;
|
|
186
|
-
if (visible.length === 0) return truncateToWidth(`${prefix}+${hidden} more`, width, "…");
|
|
187
|
-
return truncateToWidth(`${prefix}${visible.join(", ")}, +${hidden} more`, width, "…");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
class MultipleResultText {
|
|
191
|
-
private readonly answers: readonly string[];
|
|
192
|
-
private readonly expanded: boolean;
|
|
193
|
-
private readonly customAnswer: string | undefined;
|
|
194
|
-
private readonly color: (name: ThemeColor, text: string) => string;
|
|
195
|
-
|
|
196
|
-
constructor(
|
|
197
|
-
answers: readonly string[],
|
|
198
|
-
expanded: boolean,
|
|
199
|
-
customAnswer: string | undefined,
|
|
200
|
-
color: (name: ThemeColor, text: string) => string,
|
|
201
|
-
) {
|
|
202
|
-
this.answers = answers;
|
|
203
|
-
this.expanded = expanded;
|
|
204
|
-
this.customAnswer = customAnswer;
|
|
205
|
-
this.color = color;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
render(width: number): string[] {
|
|
209
|
-
if (width <= 0) return [];
|
|
210
|
-
if (!this.expanded) return [this.color("accent", compactMultipleAnswers(this.answers, width))];
|
|
211
|
-
return this.answers.flatMap((answer) => wrapTextWithAnsi(
|
|
212
|
-
`${this.color("success", "✓ ")}${answer === this.customAnswer ? this.color("muted", "(wrote) ") : ""}${this.color("accent", answer)}`,
|
|
213
|
-
width,
|
|
214
|
-
));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
invalidate(): void {}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
88
|
export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
221
89
|
const customInputHistory: string[] = [];
|
|
222
90
|
let customInputHistoryBytes = 0;
|
|
@@ -241,11 +109,6 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
241
109
|
customInputHistoryBytes += bytes;
|
|
242
110
|
return true;
|
|
243
111
|
};
|
|
244
|
-
const inputCharacterCount = (value: string): number => {
|
|
245
|
-
let count = 0;
|
|
246
|
-
for (const _character of value) count += 1;
|
|
247
|
-
return count;
|
|
248
|
-
};
|
|
249
112
|
pi.on("session_start", clearCustomInputHistory);
|
|
250
113
|
pi.on("session_tree", clearCustomInputHistory);
|
|
251
114
|
pi.on("session_shutdown", clearCustomInputHistory);
|
|
@@ -279,428 +142,19 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
279
142
|
{ label: "Type a custom answer", originalIndex: params.options.length + 1, isOther: true },
|
|
280
143
|
];
|
|
281
144
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
let completed = false;
|
|
294
|
-
|
|
295
|
-
const finish = (selection: QuestionSelection): void => {
|
|
296
|
-
if (completed) return;
|
|
297
|
-
completed = true;
|
|
298
|
-
done(selection);
|
|
299
|
-
};
|
|
300
|
-
finishFromAbort = () => finish({ kind: "aborted" });
|
|
301
|
-
|
|
302
|
-
const keyHint = (keybinding: Parameters<typeof keybindings.getKeys>[0], description: string): string => {
|
|
303
|
-
const keyText = keybindings.getKeys(keybinding)
|
|
304
|
-
.join("/")
|
|
305
|
-
.split("/")
|
|
306
|
-
.map((key) => key.split("+").map((part) => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part).join("+"))
|
|
307
|
-
.join("/");
|
|
308
|
-
return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const editorTheme: EditorTheme = {
|
|
312
|
-
borderColor: (text) => theme.fg("accent", text),
|
|
313
|
-
selectList: {
|
|
314
|
-
selectedPrefix: (text) => theme.fg("accent", text),
|
|
315
|
-
selectedText: (text) => theme.fg("accent", text),
|
|
316
|
-
description: (text) => theme.fg("muted", text),
|
|
317
|
-
scrollInfo: (text) => theme.fg("dim", text),
|
|
318
|
-
noMatch: (text) => theme.fg("warning", text),
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
const editor = new Editor(tui, editorTheme);
|
|
322
|
-
customInputHistory.forEach((value) => editor.addToHistory(value));
|
|
323
|
-
|
|
324
|
-
const filteredOptions = (): DisplayOption[] => {
|
|
325
|
-
const query = filterQuery.trim().toLowerCase();
|
|
326
|
-
return options.filter((option) => option.isOther
|
|
327
|
-
|| query.length === 0
|
|
328
|
-
|| option.label.toLowerCase().includes(query)
|
|
329
|
-
|| option.description?.toLowerCase().includes(query));
|
|
330
|
-
};
|
|
331
|
-
const selectedCount = (): number => selectedOriginalIndices.size + (customAnswer === undefined ? 0 : 1);
|
|
332
|
-
const orderedMultipleSelection = () => {
|
|
333
|
-
const selectedIndices = [...selectedOriginalIndices].sort((left, right) => left - right);
|
|
334
|
-
const predefined = selectedIndices.map((index) => {
|
|
335
|
-
const option = params.options[index - 1];
|
|
336
|
-
if (!option) throw new Error("Question selection no longer matches an available option");
|
|
337
|
-
return option.label;
|
|
338
|
-
});
|
|
339
|
-
return {
|
|
340
|
-
answers: customAnswer === undefined ? predefined : [...predefined, customAnswer],
|
|
341
|
-
selectedIndices,
|
|
342
|
-
...(customAnswer === undefined ? {} : { customAnswer }),
|
|
343
|
-
};
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
const invalidate = (): void => {
|
|
347
|
-
cachedWidth = undefined;
|
|
348
|
-
cachedRows = undefined;
|
|
349
|
-
cachedLines = undefined;
|
|
350
|
-
editor.invalidate();
|
|
351
|
-
};
|
|
352
|
-
const refresh = (): void => {
|
|
353
|
-
invalidate();
|
|
354
|
-
tui.requestRender();
|
|
355
|
-
};
|
|
356
|
-
const notifyFilterLimit = (): void => ctx.ui.notify(
|
|
357
|
-
`Question filters are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes`,
|
|
358
|
-
"error",
|
|
359
|
-
);
|
|
360
|
-
const appendFilterInput = (value: string): void => {
|
|
361
|
-
const next = filterQuery + value;
|
|
362
|
-
if (inputCharacterCount(next) > FILTER_QUERY_MAX_CHARACTERS || Buffer.byteLength(next, "utf8") > FILTER_QUERY_MAX_BYTES) {
|
|
363
|
-
notifyFilterLimit();
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
filterQuery = next;
|
|
367
|
-
optionIndex = 0;
|
|
368
|
-
refresh();
|
|
369
|
-
};
|
|
370
|
-
const togglePredefined = (option: DisplayOption): void => {
|
|
371
|
-
if (selectedOriginalIndices.has(option.originalIndex)) {
|
|
372
|
-
selectedOriginalIndices.delete(option.originalIndex);
|
|
373
|
-
refresh();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (selectedCount() >= maxSelections) {
|
|
377
|
-
ctx.ui.notify(`Select at most ${maxSelections} answer${maxSelections === 1 ? "" : "s"}`, "error");
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
selectedOriginalIndices.add(option.originalIndex);
|
|
381
|
-
refresh();
|
|
382
|
-
};
|
|
383
|
-
const enterCustomMode = (): void => {
|
|
384
|
-
editMode = "custom";
|
|
385
|
-
editor.setText(mode === "multiple" ? customAnswer ?? "" : "");
|
|
386
|
-
refresh();
|
|
387
|
-
};
|
|
388
|
-
const enterFilterMode = (): void => {
|
|
389
|
-
editMode = "filter";
|
|
390
|
-
editor.setText(filterQuery);
|
|
391
|
-
refresh();
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
editor.onSubmit = (value) => {
|
|
395
|
-
if (editMode === "filter") {
|
|
396
|
-
filterQuery = value;
|
|
397
|
-
optionIndex = 0;
|
|
398
|
-
editMode = "none";
|
|
399
|
-
editor.setText("");
|
|
400
|
-
refresh();
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
const answer = value.trim();
|
|
404
|
-
if (answer) {
|
|
405
|
-
if (inputCharacterCount(answer) > CUSTOM_INPUT_MAX_CHARACTERS) {
|
|
406
|
-
ctx.ui.notify(`Custom answers are limited to ${CUSTOM_INPUT_MAX_CHARACTERS} characters`, "error");
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
if (mode === "multiple") {
|
|
410
|
-
const addsSelection = customAnswer === undefined;
|
|
411
|
-
if (addsSelection && selectedCount() >= maxSelections) {
|
|
412
|
-
editor.setText(value);
|
|
413
|
-
ctx.ui.notify(`Select at most ${maxSelections} answer${maxSelections === 1 ? "" : "s"}`, "error");
|
|
414
|
-
refresh();
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (!rememberCustomInput(answer)) {
|
|
418
|
-
editor.setText(value);
|
|
419
|
-
ctx.ui.notify(`Custom answer history is limited to ${CUSTOM_INPUT_HISTORY_BYTES} bytes`, "error");
|
|
420
|
-
refresh();
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
customAnswer = answer;
|
|
424
|
-
editMode = "none";
|
|
425
|
-
editor.setText("");
|
|
426
|
-
refresh();
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (!rememberCustomInput(answer)) {
|
|
430
|
-
ctx.ui.notify(`Custom answer history is limited to ${CUSTOM_INPUT_HISTORY_BYTES} bytes`, "error");
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
finish({ kind: "custom", answer });
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
editMode = "none";
|
|
437
|
-
editor.setText("");
|
|
438
|
-
refresh();
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const handleEditorInput = (data: string): void => {
|
|
442
|
-
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
443
|
-
editMode = "none";
|
|
444
|
-
editor.setText("");
|
|
445
|
-
refresh();
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
const before = editor.getExpandedText();
|
|
449
|
-
editor.handleInput(data);
|
|
450
|
-
const after = editor.getExpandedText();
|
|
451
|
-
const overLimit = editMode === "filter"
|
|
452
|
-
? inputCharacterCount(after) > FILTER_QUERY_MAX_CHARACTERS || Buffer.byteLength(after, "utf8") > FILTER_QUERY_MAX_BYTES
|
|
453
|
-
: inputCharacterCount(after) > CUSTOM_INPUT_MAX_CHARACTERS;
|
|
454
|
-
if (overLimit) {
|
|
455
|
-
editor.setText(before);
|
|
456
|
-
if (editMode === "filter") notifyFilterLimit();
|
|
457
|
-
else ctx.ui.notify(`Custom answers are limited to ${CUSTOM_INPUT_MAX_CHARACTERS} characters`, "error");
|
|
458
|
-
}
|
|
459
|
-
refresh();
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const handleInput = (data: string): void => {
|
|
463
|
-
if (editMode !== "none") {
|
|
464
|
-
handleEditorInput(data);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const visibleOptions = filteredOptions();
|
|
469
|
-
if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
|
|
470
|
-
const pageSize = Math.max(1, Math.min(5, Math.ceil(Math.max(1, tui.terminal.rows - 5) / 2)));
|
|
471
|
-
if (keybindings.matches(data, "tui.select.up")) {
|
|
472
|
-
optionIndex = Math.max(0, optionIndex - 1);
|
|
473
|
-
refresh();
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (keybindings.matches(data, "tui.select.down")) {
|
|
477
|
-
optionIndex = Math.min(visibleOptions.length - 1, optionIndex + 1);
|
|
478
|
-
refresh();
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
482
|
-
optionIndex = Math.max(0, optionIndex - pageSize);
|
|
483
|
-
refresh();
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
if (keybindings.matches(data, "tui.select.pageDown")) {
|
|
487
|
-
optionIndex = Math.min(visibleOptions.length - 1, optionIndex + pageSize);
|
|
488
|
-
refresh();
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const printableInput = decodeQuestionFilterInput(data);
|
|
493
|
-
const isPasteInput = data.includes("\x1B[200~");
|
|
494
|
-
if (mode === "multiple") {
|
|
495
|
-
const selected = visibleOptions[optionIndex];
|
|
496
|
-
if (!isPasteInput && printableInput === " ") {
|
|
497
|
-
if (!selected) return;
|
|
498
|
-
if (selected.isOther) {
|
|
499
|
-
if (customAnswer !== undefined) {
|
|
500
|
-
customAnswer = undefined;
|
|
501
|
-
refresh();
|
|
502
|
-
}
|
|
503
|
-
} else togglePredefined(selected);
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
if (!isPasteInput && printableInput === "/") {
|
|
507
|
-
enterFilterMode();
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
if (!isPasteInput && printableInput && /^[1-9]$/u.test(printableInput)) {
|
|
511
|
-
const numbered = visibleOptions[Number(printableInput) - 1];
|
|
512
|
-
if (!numbered) return;
|
|
513
|
-
if (numbered.isOther) enterCustomMode();
|
|
514
|
-
else togglePredefined(numbered);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
if (keybindings.matches(data, "tui.select.confirm")) {
|
|
518
|
-
if (selected?.isOther) {
|
|
519
|
-
enterCustomMode();
|
|
520
|
-
} else if (selectedCount() < minSelections) {
|
|
521
|
-
ctx.ui.notify(`Select at least ${minSelections} answer${minSelections === 1 ? "" : "s"}`, "error");
|
|
522
|
-
} else {
|
|
523
|
-
finish({ kind: "multiple", ...orderedMultipleSelection() });
|
|
524
|
-
}
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
528
|
-
if (filterQuery) {
|
|
529
|
-
filterQuery = "";
|
|
530
|
-
optionIndex = 0;
|
|
531
|
-
refresh();
|
|
532
|
-
} else finish({ kind: "cancelled" });
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (keybindings.matches(data, "tui.select.confirm")) {
|
|
539
|
-
const selected = visibleOptions[optionIndex];
|
|
540
|
-
if (!selected) return;
|
|
541
|
-
if (selected.isOther) enterCustomMode();
|
|
542
|
-
else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
546
|
-
if (filterQuery) {
|
|
547
|
-
filterQuery = "";
|
|
548
|
-
optionIndex = 0;
|
|
549
|
-
refresh();
|
|
550
|
-
} else finish({ kind: "cancelled" });
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (keybindings.matches(data, "tui.editor.deleteCharBackward")) {
|
|
554
|
-
if (filterQuery) {
|
|
555
|
-
filterQuery = removeLastGrapheme(filterQuery);
|
|
556
|
-
optionIndex = 0;
|
|
557
|
-
refresh();
|
|
558
|
-
}
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (!isPasteInput && printableInput && /^[1-9]$/u.test(printableInput)) {
|
|
562
|
-
const selected = visibleOptions[Number(printableInput) - 1];
|
|
563
|
-
if (!selected) return;
|
|
564
|
-
if (selected.isOther) enterCustomMode();
|
|
565
|
-
else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (printableInput) appendFilterInput(printableInput);
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
const render = (width: number): string[] => {
|
|
572
|
-
if (width <= 0) return [];
|
|
573
|
-
const rowBudget = tui.terminal.rows;
|
|
574
|
-
if (rowBudget <= 0) return [];
|
|
575
|
-
if (cachedLines && cachedWidth === width && cachedRows === rowBudget) return cachedLines;
|
|
576
|
-
const visibleOptions = filteredOptions();
|
|
577
|
-
if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
|
|
578
|
-
const selected = visibleOptions[optionIndex];
|
|
579
|
-
const position = `Option ${Math.min(optionIndex + 1, visibleOptions.length)}/${visibleOptions.length}`;
|
|
580
|
-
const editorText = editor.getExpandedText();
|
|
581
|
-
const editorCount = inputCharacterCount(editorText).toLocaleString();
|
|
582
|
-
const filterCount = inputCharacterCount(editMode === "filter" ? editorText : filterQuery).toLocaleString();
|
|
583
|
-
const compactDraft = editorText.replace(/\r\n|\r|\n/gu, " ↵ ") || (editMode === "filter" ? "Type a filter" : "Type an answer");
|
|
584
|
-
const selectionRange = minSelections === maxSelections ? `${minSelections}` : `${minSelections}–${maxSelections}`;
|
|
585
|
-
const selectionStatus = `Selected ${selectedCount()} · required ${selectionRange}`;
|
|
586
|
-
const optionLabel = (option: DisplayOption, index: number): string => {
|
|
587
|
-
if (mode === "single") return `${index === optionIndex ? ">" : " "} ${index + 1}. ${option.label}`;
|
|
588
|
-
const checked = option.isOther ? customAnswer !== undefined : selectedOriginalIndices.has(option.originalIndex);
|
|
589
|
-
const label = option.isOther && customAnswer !== undefined ? `Custom: ${oneLine(customAnswer)}` : option.label;
|
|
590
|
-
return `${index === optionIndex ? ">" : " "} ${checked ? "[x]" : "[ ]"} ${index + 1}. ${label}`;
|
|
591
|
-
};
|
|
592
|
-
const browseHint = mode === "multiple"
|
|
593
|
-
? `space toggle • / filter • ${keyHint("tui.select.confirm", selected?.isOther ? (customAnswer ? "edit custom" : "add custom") : "submit")} • ${keyHint("tui.select.cancel", filterQuery ? "clear filter" : "cancel")}`
|
|
594
|
-
: `${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`;
|
|
595
|
-
const editHint = `${keyHint("tui.input.submit", editMode === "filter" ? "apply" : "submit")} • ${keyHint("tui.select.cancel", "options")}`;
|
|
596
|
-
|
|
597
|
-
let lines: string[];
|
|
598
|
-
if (rowBudget <= 2) {
|
|
599
|
-
if (editMode !== "none") lines = [`${editMode === "filter" ? "Filter" : "Answer"} ${editMode === "filter" ? filterCount : editorCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`, compactDraft];
|
|
600
|
-
else if (mode === "multiple") {
|
|
601
|
-
const focusedRow = selected ? optionLabel(selected, optionIndex) : "";
|
|
602
|
-
const compactSelectionStatus = visibleWidth(selectionStatus) <= width
|
|
603
|
-
? selectionStatus
|
|
604
|
-
: `Selected ${selectedCount()}`;
|
|
605
|
-
lines = focusedRow && visibleWidth(focusedRow) <= width
|
|
606
|
-
? [focusedRow, compactSelectionStatus]
|
|
607
|
-
: [compactSelectionStatus, focusedRow];
|
|
608
|
-
} else lines = [`${selected ? `> ${selected.label}` : "No matching options"} · ${position}`];
|
|
609
|
-
} else if (rowBudget <= 5) {
|
|
610
|
-
lines = [
|
|
611
|
-
...boundedQuestionLines(question, width, Math.max(1, rowBudget - 3)),
|
|
612
|
-
editMode !== "none"
|
|
613
|
-
? `${editMode === "filter" ? "Filter" : "Answer"} ${editMode === "filter" ? filterCount : editorCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`
|
|
614
|
-
: selected ? optionLabel(selected, optionIndex) : "No matching options",
|
|
615
|
-
editMode !== "none" ? compactDraft : mode === "multiple" ? selectionStatus : filterQuery ? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()}` : position,
|
|
616
|
-
editMode !== "none" ? editHint : browseHint,
|
|
617
|
-
];
|
|
618
|
-
} else {
|
|
619
|
-
const questionLines = boundedQuestionLines(question, width, Math.max(1, rowBudget - 5));
|
|
620
|
-
const contentRows = rowBudget - questionLines.length - 4;
|
|
621
|
-
const optionCapacity = Math.max(1, Math.min(5, Math.ceil(contentRows / 2)));
|
|
622
|
-
const detailCapacity = Math.max(0, contentRows - optionCapacity);
|
|
623
|
-
const { start, end } = visibleOptionRange(visibleOptions.length, optionIndex, optionCapacity);
|
|
624
|
-
const hiddenAbove = start > 0 ? `↑ ${start}` : "";
|
|
625
|
-
const hiddenBelowCount = visibleOptions.length - end;
|
|
626
|
-
const hiddenBelow = hiddenBelowCount > 0 ? `↓ ${hiddenBelowCount}` : "";
|
|
627
|
-
const hiddenStatus = [hiddenAbove, hiddenBelow].filter(Boolean).join(" · ");
|
|
628
|
-
const progress = editMode === "filter"
|
|
629
|
-
? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()}`
|
|
630
|
-
: editMode === "custom"
|
|
631
|
-
? `Answer ${editorCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`
|
|
632
|
-
: mode === "multiple"
|
|
633
|
-
? `${selectionStatus}${hiddenStatus ? ` · ${hiddenStatus}` : ""}`
|
|
634
|
-
: filterQuery ? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} · ${position}` : `${position}${hiddenStatus ? ` · ${hiddenStatus}` : ""}`;
|
|
635
|
-
const navigationHint = editMode !== "none" ? editHint : mode === "multiple"
|
|
636
|
-
? `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${browseHint}`
|
|
637
|
-
: `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", filterQuery ? "clear filter" : "cancel")}`;
|
|
638
|
-
lines = [
|
|
639
|
-
theme.fg("accent", "─".repeat(width)),
|
|
640
|
-
...questionLines.map((line) => theme.fg("text", line)),
|
|
641
|
-
theme.fg("muted", truncateToWidth(` ${progress}`, width, "…")),
|
|
642
|
-
];
|
|
643
|
-
if (editMode !== "none") {
|
|
644
|
-
const editorLines = editor.render(width);
|
|
645
|
-
const draftLines = editorLines.length > 2 ? editorLines.slice(1, -1) : editorLines;
|
|
646
|
-
lines.push(...(draftLines.length > 0 ? draftLines : [editMode === "filter" ? "Type a filter" : "Type an answer"]).slice(-contentRows));
|
|
647
|
-
} else {
|
|
648
|
-
for (let index = start; index < end; index += 1) {
|
|
649
|
-
const option = visibleOptions[index];
|
|
650
|
-
if (!option) continue;
|
|
651
|
-
const color: ThemeColor = index === optionIndex ? "accent" : "text";
|
|
652
|
-
lines.push(theme.fg(color, truncateToWidth(optionLabel(option, index), width, "…")));
|
|
653
|
-
}
|
|
654
|
-
const detailLines: string[] = [];
|
|
655
|
-
if (selected?.description) detailLines.push(...wrapTextWithAnsi(theme.fg("muted", selected.description), width));
|
|
656
|
-
if (selected?.preview) {
|
|
657
|
-
detailLines.push(theme.fg("accent", theme.bold("Proposal preview")));
|
|
658
|
-
detailLines.push(...new Markdown(selected.preview, 0, 0, {
|
|
659
|
-
heading: (text) => theme.fg("accent", theme.bold(text)), link: (text) => theme.fg("accent", text),
|
|
660
|
-
linkUrl: (text) => theme.fg("dim", text), code: (text) => theme.fg("mdCode", text),
|
|
661
|
-
codeBlock: (text) => theme.fg("mdCodeBlock", text), codeBlockBorder: (text) => theme.fg("mdCodeBlockBorder", text),
|
|
662
|
-
quote: (text) => theme.fg("mdQuote", text), quoteBorder: (text) => theme.fg("mdQuoteBorder", text),
|
|
663
|
-
hr: (text) => theme.fg("mdHr", text), listBullet: (text) => theme.fg("mdListBullet", text),
|
|
664
|
-
bold: (text) => theme.bold(text), italic: (text) => theme.italic(text), strikethrough: (text) => theme.strikethrough(text),
|
|
665
|
-
underline: (text) => theme.underline(text),
|
|
666
|
-
}, { color: (text) => theme.fg("muted", text) }).render(width));
|
|
667
|
-
}
|
|
668
|
-
if (detailCapacity > 0 && detailLines.length > detailCapacity) {
|
|
669
|
-
const visibleDetailRows = Math.max(0, detailCapacity - 1);
|
|
670
|
-
lines.push(...detailLines.slice(0, visibleDetailRows));
|
|
671
|
-
const hiddenRows = detailLines.length - visibleDetailRows;
|
|
672
|
-
lines.push(theme.fg("dim", `… ${hiddenRows} more line${hiddenRows === 1 ? "" : "s"}`));
|
|
673
|
-
} else lines.push(...detailLines.slice(0, detailCapacity));
|
|
674
|
-
}
|
|
675
|
-
lines.push(theme.fg("dim", truncateToWidth(` ${navigationHint}`, width, "…")));
|
|
676
|
-
lines.push(theme.fg("accent", "─".repeat(width)));
|
|
677
|
-
}
|
|
678
|
-
cachedWidth = width;
|
|
679
|
-
cachedRows = rowBudget;
|
|
680
|
-
cachedLines = lines.slice(0, rowBudget).map((line) => boundedRenderLine(line, width, rowBudget <= 5 ? "…" : ""));
|
|
681
|
-
return cachedLines;
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
let focused = false;
|
|
685
|
-
return {
|
|
686
|
-
get focused(): boolean { return focused; },
|
|
687
|
-
set focused(value: boolean) { focused = value; editor.focused = value; },
|
|
688
|
-
render,
|
|
689
|
-
handleInput,
|
|
690
|
-
invalidate,
|
|
691
|
-
};
|
|
145
|
+
const result = await openQuestionUi({
|
|
146
|
+
ctx,
|
|
147
|
+
signal,
|
|
148
|
+
question,
|
|
149
|
+
options,
|
|
150
|
+
originalOptions: params.options,
|
|
151
|
+
mode,
|
|
152
|
+
minSelections,
|
|
153
|
+
maxSelections,
|
|
154
|
+
customInputHistory,
|
|
155
|
+
rememberCustomInput,
|
|
692
156
|
});
|
|
693
157
|
|
|
694
|
-
const abortHandler = (): void => finishFromAbort?.();
|
|
695
|
-
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
696
|
-
if (signal?.aborted) abortHandler();
|
|
697
|
-
let result: QuestionSelection;
|
|
698
|
-
try {
|
|
699
|
-
result = await resultPromise;
|
|
700
|
-
} finally {
|
|
701
|
-
signal?.removeEventListener("abort", abortHandler);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
158
|
const simpleOptions = params.options.map((option) => option.label);
|
|
705
159
|
if (result.kind === "aborted") throw new Error("Question cancelled because the agent operation was aborted");
|
|
706
160
|
if (result.kind === "cancelled" && mode === "multiple") {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
/-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/u,
|
|
3
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/u,
|
|
4
|
+
/\bAIza[0-9A-Za-z_-]{35}\b/u,
|
|
5
|
+
/\bgh(?:p|o|u|s|r)_[A-Za-z0-9]{36,255}\b/u,
|
|
6
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,255}\b/u,
|
|
7
|
+
/\bglpat-[A-Za-z0-9_-]{20,255}\b/u,
|
|
8
|
+
/\bnpm_[A-Za-z0-9]{20,255}\b/u,
|
|
9
|
+
/\bsk-(?:ant-|proj-|svcacct-)?[A-Za-z0-9_-]{20,255}\b/u,
|
|
10
|
+
/\bsk_live_[A-Za-z0-9]{16,255}\b/u,
|
|
11
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,255}\b/u,
|
|
12
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/iu,
|
|
13
|
+
/^[\t ]*["']?[\w.-]*(?:api_key|apikey|password|passwd|secret|token)[\w.-]*["']?[\t ]*(?:=|:)[\t ]*(?:"[^"\r\n]+"|'[^'\r\n]+'|[^\s#][^\r\n]*)/imu,
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/** Detects high-confidence credential material without returning the matched value. */
|
|
17
|
+
export function containsLikelySecret(text: string): boolean {
|
|
18
|
+
return SECRET_PATTERNS.some((pattern) => pattern.test(text));
|
|
19
|
+
}
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -382,7 +382,7 @@ class PiCodeEditor extends CustomEditor {
|
|
|
382
382
|
for (let index = bottomBorderIndex + 1; index < lines.length; index += 1) {
|
|
383
383
|
rendered.push(` ${padRight(lines[index] ?? "", innerWidth)}`);
|
|
384
384
|
}
|
|
385
|
-
return ["", ...rendered.map((line) => truncateToWidth(line, width, ""))];
|
|
385
|
+
return [this.runtimeTheme.fg("borderMuted", "─".repeat(width)), ...rendered.map((line) => truncateToWidth(line, width, ""))];
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.21",
|
|
4
4
|
"description": "TUI, goals, and workflow automation for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@earendil-works/pi-ai": ">=0.84.3",
|
|
47
|
-
"@earendil-works/pi-coding-agent": ">=0.84.3",
|
|
48
|
-
"@earendil-works/pi-tui": ">=0.84.3",
|
|
46
|
+
"@earendil-works/pi-ai": ">=0.84.3 <1",
|
|
47
|
+
"@earendil-works/pi-coding-agent": ">=0.84.3 <1",
|
|
48
|
+
"@earendil-works/pi-tui": ">=0.84.3 <1",
|
|
49
49
|
"typebox": ">=1.1.38 <2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@earendil-works/pi-tui": "0.84.3",
|
|
55
55
|
"@types/node": "24.12.4",
|
|
56
56
|
"eslint": "^10.9.1",
|
|
57
|
-
"typebox": "1.
|
|
57
|
+
"typebox": "1.3.20",
|
|
58
58
|
"typescript": "5.9.3",
|
|
59
59
|
"typescript-eslint": "^8.68.0"
|
|
60
60
|
},
|