pi-soly 1.10.0 → 1.11.0
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/ask/index.ts +0 -11
- package/ask/picker.ts +281 -176
- package/ask/tests/picker.test.ts +177 -148
- package/codemap.ts +276 -0
- package/hotreload.ts +239 -0
- package/init.ts +302 -0
- package/mcp/CHANGELOG.md +384 -0
- package/mcp/LICENSE +21 -0
- package/mcp/OAUTH.md +355 -0
- package/mcp/README.md +410 -0
- package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
- package/mcp/__tests__/cli.upstream-test.ts +97 -0
- package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
- package/mcp/__tests__/config.upstream-test.ts +303 -0
- package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
- package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
- package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
- package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
- package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
- package/mcp/__tests__/errors.upstream-test.ts +218 -0
- package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
- package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
- package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
- package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
- package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
- package/mcp/__tests__/logger.upstream-test.ts +175 -0
- package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
- package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
- package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
- package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
- package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
- package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
- package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
- package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
- package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
- package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
- package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
- package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
- package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
- package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
- package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
- package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
- package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
- package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
- package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
- package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
- package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
- package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
- package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
- package/mcp/agent-dir.ts +20 -0
- package/mcp/app-bridge.bundle.js +67 -0
- package/mcp/cli.js +184 -0
- package/mcp/commands.ts +422 -0
- package/mcp/config.ts +666 -0
- package/mcp/consent-manager.ts +64 -0
- package/mcp/direct-tools.ts +439 -0
- package/mcp/elicitation-handler.ts +347 -0
- package/mcp/errors.ts +219 -0
- package/mcp/glimpse-ui.ts +80 -0
- package/mcp/host-html-template.ts +427 -0
- package/mcp/index.ts +362 -0
- package/mcp/init.ts +362 -0
- package/mcp/lifecycle.ts +93 -0
- package/mcp/logger.ts +169 -0
- package/mcp/mcp-auth-flow.ts +559 -0
- package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
- package/mcp/mcp-auth.ts +302 -0
- package/mcp/mcp-auth.upstream-test.ts +373 -0
- package/mcp/mcp-callback-server.ts +372 -0
- package/mcp/mcp-callback-server.upstream-test.ts +416 -0
- package/mcp/mcp-oauth-provider.ts +369 -0
- package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
- package/mcp/mcp-panel.ts +829 -0
- package/mcp/mcp-setup-panel.ts +580 -0
- package/mcp/metadata-cache.ts +201 -0
- package/mcp/notify.ts +111 -0
- package/mcp/npx-resolver.ts +424 -0
- package/mcp/oauth-handler.ts +57 -0
- package/mcp/onboarding-state.ts +68 -0
- package/mcp/package.json +106 -0
- package/mcp/panel-keys.ts +37 -0
- package/mcp/proxy-modes.ts +949 -0
- package/mcp/resource-tools.ts +17 -0
- package/mcp/sampling-handler.ts +268 -0
- package/mcp/server-manager.ts +545 -0
- package/mcp/state.ts +41 -0
- package/mcp/tool-metadata.ts +216 -0
- package/mcp/tool-registrar.ts +46 -0
- package/mcp/tool-result-renderer.ts +161 -0
- package/mcp/types.ts +448 -0
- package/mcp/ui-resource-handler.ts +146 -0
- package/mcp/ui-server.ts +623 -0
- package/mcp/ui-session.ts +386 -0
- package/mcp/ui-stream-types.ts +89 -0
- package/mcp/utils.ts +129 -0
- package/mcp/vitest.config.ts +14 -0
- package/migrate.ts +258 -0
- package/notification.ts +218 -0
- package/notifications-log.ts +83 -0
- package/package.json +20 -3
- package/status.ts +140 -0
package/ask/tests/picker.test.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/// <reference types="bun-types" />
|
|
11
11
|
import { describe, test, expect } from "bun:test";
|
|
12
12
|
import { AskProComponent, type AskQuestion, type AskProResult, type AskProTheme } from "../picker.js";
|
|
13
|
-
import type
|
|
13
|
+
import { visibleWidth, type KeybindingsManager } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
15
|
// Minimal theme: just enough for the picker to render without errors.
|
|
16
16
|
const theme: AskProTheme = {
|
|
@@ -18,13 +18,17 @@ const theme: AskProTheme = {
|
|
|
18
18
|
bold: (text: string) => `**${text}**`,
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
// Minimal keybindings:
|
|
21
|
+
// Minimal keybindings: recognize the bindings the picker AND the inline
|
|
22
|
+
// Input field use, so tests exercise real key handling (backspace, ^U, etc.).
|
|
22
23
|
const keybindings = {
|
|
23
24
|
matches: (keyData: string, name: string) => {
|
|
24
25
|
if (name === "tui.select.up") return keyData === "\x1b[A" || keyData === "k";
|
|
25
26
|
if (name === "tui.select.down") return keyData === "\x1b[B" || keyData === "j";
|
|
26
27
|
if (name === "tui.select.confirm") return keyData === "\n" || keyData === "\r";
|
|
27
28
|
if (name === "tui.select.cancel") return keyData === "\x1b";
|
|
29
|
+
// Inline Input editing gestures (mirror pi-tui defaults)
|
|
30
|
+
if (name === "tui.editor.deleteCharBackward") return keyData === "\x7f"; // backspace
|
|
31
|
+
if (name === "tui.editor.deleteToLineStart") return keyData === "\x15"; // ctrl+u
|
|
28
32
|
return false;
|
|
29
33
|
},
|
|
30
34
|
} as unknown as KeybindingsManager;
|
|
@@ -397,11 +401,7 @@ describe("AskProComponent — Other… option (allowOther)", () => {
|
|
|
397
401
|
},
|
|
398
402
|
];
|
|
399
403
|
|
|
400
|
-
function
|
|
401
|
-
questions: AskQuestion[],
|
|
402
|
-
mockInput: (text: string | undefined) => Promise<string | undefined>,
|
|
403
|
-
) {
|
|
404
|
-
const inputCalls: Array<{ title: string; prompt: string; placeholder?: string }> = [];
|
|
404
|
+
function setupWithOther(questions: AskQuestion[]) {
|
|
405
405
|
let doneResult: AskProResult | null = null;
|
|
406
406
|
const picker = new AskProComponent({
|
|
407
407
|
questions,
|
|
@@ -410,78 +410,69 @@ describe("AskProComponent — Other… option (allowOther)", () => {
|
|
|
410
410
|
done: (r) => {
|
|
411
411
|
doneResult = r;
|
|
412
412
|
},
|
|
413
|
-
onRequestInput: async (req) => {
|
|
414
|
-
inputCalls.push(req);
|
|
415
|
-
return mockInput("user typed text");
|
|
416
|
-
},
|
|
417
413
|
});
|
|
418
414
|
return {
|
|
419
415
|
picker,
|
|
420
416
|
getDone: (): AskProResult | null => doneResult,
|
|
421
417
|
getAnswers: () => picker.getAnswers(),
|
|
422
|
-
getInputCalls: () => inputCalls,
|
|
423
418
|
};
|
|
424
419
|
}
|
|
425
420
|
|
|
421
|
+
/** Simulate typing `text` into the active inline field, then Enter to
|
|
422
|
+
* commit. Mirrors how the real TUI feeds one character per keypress. */
|
|
423
|
+
function typeInline(picker: AskProComponent, text: string, commit = true): void {
|
|
424
|
+
for (const ch of text) picker.handleInput(ch);
|
|
425
|
+
if (commit) picker.handleInput("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
426
428
|
test("Other… appears as last option when allowOther=true", () => {
|
|
427
|
-
const { picker } =
|
|
429
|
+
const { picker } = setupWithOther(OTHER_QUESTIONS);
|
|
428
430
|
picker.handleInput("j"); // 0 → 1 (JWT localStorage)
|
|
429
431
|
picker.handleInput("j"); // 1 → 2 (Other…)
|
|
430
432
|
// 2 real options + Other = index 2
|
|
431
433
|
expect(picker.getSelectedIndex()).toBe(2);
|
|
432
434
|
});
|
|
433
435
|
|
|
434
|
-
test("number key (3) on Other…
|
|
435
|
-
const { picker,
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
);
|
|
439
|
-
picker.handleInput("3"); // pick Other…
|
|
440
|
-
// requestOtherInput is async; wait a microtask for the await
|
|
441
|
-
await new Promise((r) => setImmediate(r));
|
|
442
|
-
expect(getInputCalls().length).toBe(1);
|
|
443
|
-
expect(getInputCalls()[0]?.title).toBe("Auth");
|
|
444
|
-
expect(getInputCalls()[0]?.prompt).toContain("Which auth?");
|
|
445
|
-
// Answer should now be the custom string
|
|
436
|
+
test("number key (3) on Other… opens the inline field; Enter commits it", () => {
|
|
437
|
+
const { picker, getAnswers } = setupWithOther(OTHER_QUESTIONS);
|
|
438
|
+
picker.handleInput("3"); // open Other… inline field
|
|
439
|
+
typeInline(picker, "JWT via custom OAuth2 proxy");
|
|
446
440
|
expect(getAnswers().get(0)).toBe("JWT via custom OAuth2 proxy");
|
|
447
441
|
});
|
|
448
442
|
|
|
449
|
-
test("Enter on Other…
|
|
450
|
-
const { picker,
|
|
451
|
-
OTHER_QUESTIONS,
|
|
452
|
-
async () => "magic-link via email",
|
|
453
|
-
);
|
|
443
|
+
test("Enter on Other… (after selecting it) opens the inline field", () => {
|
|
444
|
+
const { picker, getAnswers } = setupWithOther(OTHER_QUESTIONS);
|
|
454
445
|
picker.handleInput("j"); // 0 → 1
|
|
455
446
|
picker.handleInput("j"); // 1 → 2 (Other…)
|
|
456
|
-
picker.handleInput("\n");
|
|
457
|
-
|
|
458
|
-
expect(getInputCalls().length).toBe(1);
|
|
447
|
+
picker.handleInput("\n"); // open the inline field
|
|
448
|
+
typeInline(picker, "magic-link via email");
|
|
459
449
|
expect(getAnswers().get(0)).toBe("magic-link via email");
|
|
460
450
|
});
|
|
461
451
|
|
|
462
|
-
test("
|
|
463
|
-
const { picker, getAnswers } =
|
|
464
|
-
picker.handleInput("3");
|
|
465
|
-
|
|
452
|
+
test("cancelling inline input (Esc) leaves answer unchanged", () => {
|
|
453
|
+
const { picker, getAnswers } = setupWithOther(OTHER_QUESTIONS);
|
|
454
|
+
picker.handleInput("3"); // open Other… inline field
|
|
455
|
+
picker.handleInput("J"); // type something
|
|
456
|
+
picker.handleInput("\x1b"); // Esc — cancel
|
|
466
457
|
expect(getAnswers().get(0)).toBeUndefined();
|
|
467
458
|
});
|
|
468
459
|
|
|
469
|
-
test("empty
|
|
470
|
-
const { picker, getAnswers } =
|
|
471
|
-
picker.handleInput("3");
|
|
472
|
-
|
|
460
|
+
test("empty inline submission is ignored (no answer stored)", () => {
|
|
461
|
+
const { picker, getAnswers } = setupWithOther(OTHER_QUESTIONS);
|
|
462
|
+
picker.handleInput("3"); // open Other… inline field
|
|
463
|
+
typeInline(picker, " "); // only spaces → trimmed empty → not stored
|
|
473
464
|
expect(getAnswers().get(0)).toBeUndefined();
|
|
474
465
|
});
|
|
475
466
|
|
|
476
467
|
test("arrow down stops at Other… (not past it)", () => {
|
|
477
|
-
const { picker } =
|
|
468
|
+
const { picker } = setupWithOther(OTHER_QUESTIONS);
|
|
478
469
|
picker.handleInput("j"); // 1
|
|
479
470
|
picker.handleInput("j"); // 2 (Other…)
|
|
480
471
|
picker.handleInput("j"); // stays at 2
|
|
481
472
|
expect(picker.getSelectedIndex()).toBe(2);
|
|
482
473
|
});
|
|
483
474
|
|
|
484
|
-
test("Other… picks auto-advance to next question (single-select)",
|
|
475
|
+
test("Other… picks auto-advance to next question (single-select)", () => {
|
|
485
476
|
const TWO_Q: AskQuestion[] = [
|
|
486
477
|
{
|
|
487
478
|
header: "Q1",
|
|
@@ -495,15 +486,14 @@ describe("AskProComponent — Other… option (allowOther)", () => {
|
|
|
495
486
|
options: [{ label: "X" }],
|
|
496
487
|
},
|
|
497
488
|
];
|
|
498
|
-
const { picker, getAnswers } =
|
|
499
|
-
picker.handleInput("2"); //
|
|
500
|
-
|
|
501
|
-
// Should be on Q2 now
|
|
489
|
+
const { picker, getAnswers } = setupWithOther(TWO_Q);
|
|
490
|
+
picker.handleInput("2"); // open Other… on Q1
|
|
491
|
+
typeInline(picker, "custom A"); // commit → advance to Q2
|
|
502
492
|
expect(picker.getCurrentIndex()).toBe(1);
|
|
503
493
|
expect(getAnswers().get(0)).toBe("custom A");
|
|
504
494
|
});
|
|
505
495
|
|
|
506
|
-
test("Other… picks submit if on last question",
|
|
496
|
+
test("Other… picks submit if on last question", () => {
|
|
507
497
|
const ONE_Q: AskQuestion[] = [
|
|
508
498
|
{
|
|
509
499
|
header: "Only",
|
|
@@ -512,13 +502,13 @@ describe("AskProComponent — Other… option (allowOther)", () => {
|
|
|
512
502
|
allowOther: true,
|
|
513
503
|
},
|
|
514
504
|
];
|
|
515
|
-
const { picker, getDone } =
|
|
516
|
-
picker.handleInput("2"); // Other…
|
|
517
|
-
|
|
505
|
+
const { picker, getDone } = setupWithOther(ONE_Q);
|
|
506
|
+
picker.handleInput("2"); // open Other…
|
|
507
|
+
typeInline(picker, "freeform"); // commit → submit (last question)
|
|
518
508
|
expect(getDone()).toEqual({ answers: { 0: "freeform" } });
|
|
519
509
|
});
|
|
520
510
|
|
|
521
|
-
test("Other… in multi-select pushes the string into the array",
|
|
511
|
+
test("Other… in multi-select pushes the string into the array", () => {
|
|
522
512
|
const MULTI_WITH_OTHER: AskQuestion[] = [
|
|
523
513
|
{
|
|
524
514
|
header: "Pick",
|
|
@@ -528,10 +518,10 @@ describe("AskProComponent — Other… option (allowOther)", () => {
|
|
|
528
518
|
allowOther: true,
|
|
529
519
|
},
|
|
530
520
|
];
|
|
531
|
-
const { picker, getAnswers } =
|
|
521
|
+
const { picker, getAnswers } = setupWithOther(MULTI_WITH_OTHER);
|
|
532
522
|
picker.handleInput("1"); // toggle A
|
|
533
|
-
picker.handleInput("3"); //
|
|
534
|
-
|
|
523
|
+
picker.handleInput("3"); // open Other… inline field (Space would too)
|
|
524
|
+
typeInline(picker, "my custom"); // commit
|
|
535
525
|
// A is toggled + custom string
|
|
536
526
|
const a = getAnswers().get(0);
|
|
537
527
|
expect(Array.isArray(a)).toBe(true);
|
|
@@ -540,7 +530,7 @@ describe("AskProComponent — Other… option (allowOther)", () => {
|
|
|
540
530
|
expect(picker.getCurrentIndex()).toBe(0);
|
|
541
531
|
});
|
|
542
532
|
|
|
543
|
-
test("re-picking Other… replaces the previous custom string (not appends)",
|
|
533
|
+
test("re-picking Other… replaces the previous custom string (not appends)", () => {
|
|
544
534
|
const ONE_Q: AskQuestion[] = [
|
|
545
535
|
{
|
|
546
536
|
header: "Only",
|
|
@@ -549,41 +539,35 @@ describe("AskProComponent — Other… option (allowOther)", () => {
|
|
|
549
539
|
allowOther: true,
|
|
550
540
|
},
|
|
551
541
|
];
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const val = callCount++ === 0 ? "first" : "second";
|
|
565
|
-
inputCalls.push(val);
|
|
566
|
-
return val;
|
|
542
|
+
const { picker, getAnswers } = setupWithOther(ONE_Q);
|
|
543
|
+
// First: open Other…, type "first", commit (stays — only 1 question;
|
|
544
|
+
// single-select commit on last question does NOT submit until all
|
|
545
|
+
// answered, but here it's answered so it submits). To test replace,
|
|
546
|
+
// use a multi-select question so we stay put after committing.
|
|
547
|
+
const MULTI: AskQuestion[] = [
|
|
548
|
+
{
|
|
549
|
+
header: "Pick",
|
|
550
|
+
question: "?",
|
|
551
|
+
options: [{ label: "A" }, { label: "B" }],
|
|
552
|
+
multiSelect: true,
|
|
553
|
+
allowOther: true,
|
|
567
554
|
},
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
expect(getAnswers().get(0)).toBeUndefined();
|
|
585
|
-
// selectedIndex clamps to the last real option
|
|
586
|
-
expect(picker.getSelectedIndex()).toBeLessThan(2);
|
|
555
|
+
];
|
|
556
|
+
const { picker: mp, getAnswers: getMulti } = setupWithOther(MULTI);
|
|
557
|
+
mp.handleInput("3"); // open Other… (pre-fills with "first", cursor at end)
|
|
558
|
+
mp.handleInput("\x15"); // ^U — delete to line start → ""
|
|
559
|
+
typeInline(mp, "second");
|
|
560
|
+
expect(getMulti().get(0)).toEqual(["second"]);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("allowOther without any external callback still shows Other…", () => {
|
|
564
|
+
// The inline field is built in, so Other… no longer needs a host
|
|
565
|
+
// callback. This is the whole point of the inline redesign: the
|
|
566
|
+
// host UI's modal input would tear down the picker.
|
|
567
|
+
const { picker, getAnswers } = setup(OTHER_QUESTIONS);
|
|
568
|
+
picker.handleInput("3"); // open Other… inline field
|
|
569
|
+
typeInline(picker, "plain custom");
|
|
570
|
+
expect(getAnswers().get(0)).toBe("plain custom");
|
|
587
571
|
});
|
|
588
572
|
});
|
|
589
573
|
|
|
@@ -655,41 +639,15 @@ describe("AskProComponent — option previews", () => {
|
|
|
655
639
|
// ---------------------------------------------------------------------------
|
|
656
640
|
|
|
657
641
|
describe("AskProComponent — notes (n key)", () => {
|
|
658
|
-
test("n
|
|
642
|
+
test("n opens the inline note field", () => {
|
|
659
643
|
const { picker } = setup();
|
|
660
|
-
//
|
|
644
|
+
// `n` should not throw and should not advance/change answers
|
|
661
645
|
expect(() => picker.handleInput("n")).not.toThrow();
|
|
646
|
+
expect(picker.getAnswers().size).toBe(0);
|
|
662
647
|
});
|
|
663
648
|
|
|
664
|
-
test("
|
|
665
|
-
let noteRequestCount = 0;
|
|
666
|
-
let resolveNote: (value: string | undefined) => void = () => {};
|
|
667
|
-
const picker = new AskProComponent({
|
|
668
|
-
questions: sampleQuestions,
|
|
669
|
-
theme,
|
|
670
|
-
keybindings,
|
|
671
|
-
done: () => {},
|
|
672
|
-
onRequestNote: async () => {
|
|
673
|
-
noteRequestCount++;
|
|
674
|
-
return new Promise<string | undefined>((r) => {
|
|
675
|
-
resolveNote = r;
|
|
676
|
-
});
|
|
677
|
-
},
|
|
678
|
-
});
|
|
679
|
-
// `n` should trigger note request
|
|
680
|
-
expect(noteRequestCount).toBe(0);
|
|
681
|
-
picker.handleInput("n");
|
|
682
|
-
// Microtask to let async start
|
|
683
|
-
await Promise.resolve();
|
|
684
|
-
expect(noteRequestCount).toBe(1);
|
|
685
|
-
// Resolve with a note
|
|
686
|
-
resolveNote("only for prod, use TLS");
|
|
687
|
-
await Promise.resolve();
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
test("note is included in submit result", async () => {
|
|
649
|
+
test("typed note is stored on Enter and included in submit result", () => {
|
|
691
650
|
let doneResult: AskProResult | null = null;
|
|
692
|
-
const pendingNotes: Array<(v: string | undefined) => void> = [];
|
|
693
651
|
const picker = new AskProComponent({
|
|
694
652
|
questions: sampleQuestions,
|
|
695
653
|
theme,
|
|
@@ -697,24 +655,13 @@ describe("AskProComponent — notes (n key)", () => {
|
|
|
697
655
|
done: (r) => {
|
|
698
656
|
doneResult = r;
|
|
699
657
|
},
|
|
700
|
-
onRequestNote: async () => {
|
|
701
|
-
return new Promise<string | undefined>((resolve) => {
|
|
702
|
-
pendingNotes.push(resolve);
|
|
703
|
-
});
|
|
704
|
-
},
|
|
705
658
|
});
|
|
706
659
|
// Pick Q1 → advance to Q2
|
|
707
660
|
picker.handleInput("1");
|
|
708
|
-
//
|
|
661
|
+
// Open the inline note field on Q2 and type a note
|
|
709
662
|
picker.handleInput("n");
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
await Promise.resolve();
|
|
713
|
-
expect(pendingNotes.length).toBe(1);
|
|
714
|
-
pendingNotes[0]!("rotate keys monthly");
|
|
715
|
-
// Wait for the dialog to settle
|
|
716
|
-
await Promise.resolve();
|
|
717
|
-
await Promise.resolve();
|
|
663
|
+
for (const ch of "rotate keys monthly") picker.handleInput(ch);
|
|
664
|
+
picker.handleInput("\n"); // commit
|
|
718
665
|
// Submit Q2 (last question, all answered)
|
|
719
666
|
picker.handleInput("1");
|
|
720
667
|
expect(doneResult).not.toBeNull();
|
|
@@ -723,28 +670,110 @@ describe("AskProComponent — notes (n key)", () => {
|
|
|
723
670
|
expect(result?.notes?.[1]).toBe("rotate keys monthly");
|
|
724
671
|
});
|
|
725
672
|
|
|
726
|
-
test("
|
|
727
|
-
|
|
673
|
+
test("Esc cancels the note field, leaving no note", () => {
|
|
674
|
+
const { picker } = setup();
|
|
675
|
+
picker.handleInput("n");
|
|
676
|
+
for (const ch of "ignored note") picker.handleInput(ch);
|
|
677
|
+
picker.handleInput("\x1b"); // Esc — cancel
|
|
678
|
+
// No note stored; picker still usable (pick an option to prove it)
|
|
679
|
+
picker.handleInput("1");
|
|
680
|
+
expect(picker.getAnswers().get(0)).toBe(0);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("empty note submission clears any existing note", () => {
|
|
684
|
+
let doneResult: AskProResult | null = null;
|
|
728
685
|
const picker = new AskProComponent({
|
|
729
686
|
questions: sampleQuestions,
|
|
730
687
|
theme,
|
|
731
688
|
keybindings,
|
|
732
|
-
done: () => {
|
|
733
|
-
|
|
734
|
-
return new Promise<string | undefined>((r) => {
|
|
735
|
-
resolveNote = r;
|
|
736
|
-
});
|
|
689
|
+
done: (r) => {
|
|
690
|
+
doneResult = r;
|
|
737
691
|
},
|
|
738
692
|
});
|
|
739
|
-
//
|
|
693
|
+
// Q1: add a note, then re-open and clear it (^U + Enter)
|
|
740
694
|
picker.handleInput("n");
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
695
|
+
for (const ch of "first note") picker.handleInput(ch);
|
|
696
|
+
picker.handleInput("\n");
|
|
697
|
+
picker.handleInput("n");
|
|
698
|
+
picker.handleInput("\x15"); // ^U — delete to line start
|
|
699
|
+
picker.handleInput("\n"); // commit empty → note cleared
|
|
700
|
+
// Pick Q1 → Q2 → submit; result must NOT carry notes
|
|
701
|
+
picker.handleInput("1");
|
|
702
|
+
picker.handleInput("1");
|
|
703
|
+
expect(doneResult as AskProResult | null).toEqual({ answers: { 0: 0, 1: 0 } });
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("note field does not break render", () => {
|
|
707
|
+
const { picker } = setup();
|
|
744
708
|
picker.handleInput("n");
|
|
745
|
-
resolveNote(" ");
|
|
746
|
-
await Promise.resolve();
|
|
747
|
-
// No crash — note cleared
|
|
748
709
|
expect(() => picker.render(80)).not.toThrow();
|
|
749
710
|
});
|
|
750
711
|
});
|
|
712
|
+
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
// Regression: render() must NEVER emit a line wider than `width`.
|
|
715
|
+
// Previously the side-by-side preview layout measured width with a regex
|
|
716
|
+
// that ignored OSC 8 / FTCS escape sequences (which pi-tui wraps around
|
|
717
|
+
// rendered text) and never truncated the left column or the combined row —
|
|
718
|
+
// so a long option label or preview word pushed a line past the terminal
|
|
719
|
+
// width and crashed pi ("Rendered line N exceeds terminal width").
|
|
720
|
+
// See C:/Users/bradw/.pi/agent/pi-crash.log.
|
|
721
|
+
// ---------------------------------------------------------------------------
|
|
722
|
+
describe("AskProComponent — render never exceeds width (crash regression)", () => {
|
|
723
|
+
// Passthrough theme: no ANSI, so visibleWidth() reports true column width.
|
|
724
|
+
const plainTheme: AskProTheme = { fg: (_c, t) => t, bold: (t) => t };
|
|
725
|
+
const kb = { matches: () => false } as unknown as KeybindingsManager;
|
|
726
|
+
|
|
727
|
+
const LONG_PREVIEW =
|
|
728
|
+
"Set-Cookie: refresh_token=xxx; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=2592000\n" +
|
|
729
|
+
"// this is a fairly long comment line that must wrap and never overflow the column";
|
|
730
|
+
|
|
731
|
+
function makePicker(preview: string, label = "short"): AskProComponent {
|
|
732
|
+
return new AskProComponent({
|
|
733
|
+
questions: [
|
|
734
|
+
{
|
|
735
|
+
header: "X",
|
|
736
|
+
question: "Pick one?",
|
|
737
|
+
options: [
|
|
738
|
+
{ label, description: "desc", preview },
|
|
739
|
+
{ label: "other option", description: "desc2" },
|
|
740
|
+
],
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
theme: plainTheme,
|
|
744
|
+
keybindings: kb,
|
|
745
|
+
done: () => {},
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
for (const width of [134, 100, 80, 60, 45, 40, 30, 20, 10]) {
|
|
750
|
+
test(`every line ≤ width=${width} with a long preview + long label`, () => {
|
|
751
|
+
const picker = makePicker(
|
|
752
|
+
LONG_PREVIEW,
|
|
753
|
+
"a-very-long-option-label-without-spaces-to-stress-truncation",
|
|
754
|
+
);
|
|
755
|
+
const lines = picker.render(width);
|
|
756
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
757
|
+
for (const line of lines) {
|
|
758
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
test("every line ≤ width with a single 500-char unbreakable token", () => {
|
|
764
|
+
const picker = makePicker("x".repeat(500), "ok");
|
|
765
|
+
for (const width of [134, 50, 25, 12]) {
|
|
766
|
+
const lines = picker.render(width);
|
|
767
|
+
for (const line of lines) {
|
|
768
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("preview content still rendered (not dropped) at a comfortable width", () => {
|
|
774
|
+
const picker = makePicker("CREATE TABLE users (id INT);", "Relational");
|
|
775
|
+
const joined = picker.render(100).join("\n");
|
|
776
|
+
expect(joined).toContain("CREATE TABLE");
|
|
777
|
+
expect(joined).toContain("preview");
|
|
778
|
+
});
|
|
779
|
+
});
|