gentle-pi 3.3.0 → 3.5.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/README.md +88 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/bin/gentle-shell.mjs +198 -0
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-agents-activity.md +95 -0
- package/docs/gentle-shell.md +26 -2
- package/docs/readme-reference.md +99 -6
- package/extensions/ask-user-choice.ts +70 -22
- package/extensions/ask-user-question.ts +338 -0
- package/extensions/gentle-agents.ts +41 -1
- package/extensions/gentle-ai.ts +59 -18
- package/extensions/gentle-shell.ts +99 -10
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-rpc-publisher.ts +342 -0
- package/lib/agents-runner.ts +7 -2
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/gentle-shell-launcher.ts +482 -0
- package/lib/inprocess-reviewer.ts +38 -1
- package/lib/native-review-cli.ts +36 -10
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +10 -0
- package/lib/review-integration-v2.ts +4 -1
- package/lib/rpc-host.ts +36 -0
- package/lib/shell-bar.ts +13 -0
- package/lib/shell-sidebar-layout.ts +10 -4
- package/lib/shell-usage-view.ts +5 -2
- package/lib/shell-usage.ts +120 -6
- package/package.json +5 -1
- package/runtime/gentle-shell-launcher.mjs +483 -0
- package/runtime/native-review-cli.mjs +35 -9
- package/runtime/review-integration-v2.mjs +4 -1
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +14 -7
- package/scripts/install-tui-mode-setting.mjs +78 -1
- package/scripts/verify-package-files.mjs +6 -3
- package/tests/agents-rpc-publisher.test.ts +407 -0
- package/tests/agents-runner.test.ts +10 -0
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-choice.test.ts +129 -0
- package/tests/ask-user-question.test.ts +661 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +9 -0
- package/tests/gentle-agents.test.ts +161 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +56 -4
- package/tests/gentle-shell-bin.test.ts +188 -0
- package/tests/gentle-shell-launcher.test.ts +718 -0
- package/tests/gentle-shell.test.ts +355 -2
- package/tests/inprocess-reviewer.test.ts +92 -0
- package/tests/install-tui-mode-guard.test.ts +99 -0
- package/tests/install-tui-mode-setting.test.ts +39 -1
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay.test.ts +21 -0
- package/tests/review-integration-v2.test.ts +30 -0
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +107 -2
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/rpc-host.test.ts +77 -0
- package/tests/shell-bar.test.ts +8 -0
- package/tests/shell-sidebar-layout.test.ts +60 -5
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/skills/release/SKILL.md +0 -137
|
@@ -133,6 +133,47 @@ const options = [
|
|
|
133
133
|
{ label: "Preserve requested hash", description: "Keep the hash from the original request.", value: "preserve_requested_hash" },
|
|
134
134
|
];
|
|
135
135
|
|
|
136
|
+
const INTERACTIVE_HOST_ENV = "GENTLE_SHELL_INTERACTIVE_HOST";
|
|
137
|
+
|
|
138
|
+
function withInteractiveHostEnv(t: { after(fn: () => void): void }): void {
|
|
139
|
+
const previous = process.env[INTERACTIVE_HOST_ENV];
|
|
140
|
+
process.env[INTERACTIVE_HOST_ENV] = "1";
|
|
141
|
+
t.after(() => {
|
|
142
|
+
if (previous === undefined) delete process.env[INTERACTIVE_HOST_ENV];
|
|
143
|
+
else process.env[INTERACTIVE_HOST_ENV] = previous;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Fake interactive-RPC-host ctx: scripted `select`/`input` answers, one per call. */
|
|
148
|
+
function rpcHostContext(selectAnswers: readonly (string | undefined)[], inputAnswers: readonly (string | undefined)[] = []) {
|
|
149
|
+
const selectCalls: Array<{ title: string; options: string[] }> = [];
|
|
150
|
+
const inputCalls: Array<{ title: string; placeholder?: string }> = [];
|
|
151
|
+
let selectIndex = 0;
|
|
152
|
+
let inputIndex = 0;
|
|
153
|
+
return {
|
|
154
|
+
ctx: {
|
|
155
|
+
mode: "rpc",
|
|
156
|
+
hasUI: true,
|
|
157
|
+
ui: {
|
|
158
|
+
select: async (title: string, opts: string[]) => {
|
|
159
|
+
selectCalls.push({ title, options: opts });
|
|
160
|
+
const answer = selectAnswers[selectIndex];
|
|
161
|
+
selectIndex += 1;
|
|
162
|
+
return answer;
|
|
163
|
+
},
|
|
164
|
+
input: async (title: string, placeholder?: string) => {
|
|
165
|
+
inputCalls.push({ title, placeholder });
|
|
166
|
+
const answer = inputAnswers[inputIndex];
|
|
167
|
+
inputIndex += 1;
|
|
168
|
+
return answer;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
selectCalls,
|
|
173
|
+
inputCalls,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
136
177
|
function tuiContext(inputs: readonly string[], rendered: { value: string }) {
|
|
137
178
|
return {
|
|
138
179
|
mode: "tui",
|
|
@@ -483,6 +524,80 @@ test("ask_user_choice cancels without a value and remains unavailable outside th
|
|
|
483
524
|
);
|
|
484
525
|
});
|
|
485
526
|
|
|
527
|
+
test("ask_user_choice stays unavailable on a plain rpc host without the interactive-host variable", async (t) => {
|
|
528
|
+
const { tool } = registerChoiceTool();
|
|
529
|
+
t.after(() => { delete process.env[INTERACTIVE_HOST_ENV]; });
|
|
530
|
+
delete process.env[INTERACTIVE_HOST_ENV];
|
|
531
|
+
|
|
532
|
+
await assert.rejects(
|
|
533
|
+
() => tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, { mode: "rpc", hasUI: true, ui: { select: async () => undefined } }),
|
|
534
|
+
/unavailable outside the interactive TUI/,
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
/** Expected `gentle-pi:ask-user-choice:blocked` sequence around one RPC-dialog turn. */
|
|
539
|
+
const RPC_BLOCKED_SEQUENCE = [
|
|
540
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
|
|
541
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
test("ask_user_choice resolves a selection through RPC dialogs on an interactive host", async (t) => {
|
|
545
|
+
withInteractiveHostEnv(t);
|
|
546
|
+
const registration = registerChoiceTool();
|
|
547
|
+
const { tool } = registration;
|
|
548
|
+
const { ctx, selectCalls } = rpcHostContext([options[1]!.label]);
|
|
549
|
+
|
|
550
|
+
const result = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, ctx);
|
|
551
|
+
|
|
552
|
+
assert.equal(selectCalls.length, 1);
|
|
553
|
+
assert.equal(selectCalls[0]?.title, "Proceed?");
|
|
554
|
+
assert.deepEqual(selectCalls[0]?.options, [options[0]!.label, options[1]!.label]);
|
|
555
|
+
assert.deepEqual(result.details.selection, { value: options[1]!.value, label: options[1]!.label, index: 2 });
|
|
556
|
+
assert.equal(result.content[0]?.text, `User selected: 2. ${options[1]!.label} (value: ${options[1]!.value})`);
|
|
557
|
+
assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("ask_user_choice resolves an Other… custom response through ctx.ui.input on an interactive host", async (t) => {
|
|
561
|
+
withInteractiveHostEnv(t);
|
|
562
|
+
const registration = registerChoiceTool();
|
|
563
|
+
const { tool } = registration;
|
|
564
|
+
const { ctx, selectCalls, inputCalls } = rpcHostContext(["Other…"], ["a free-form reply"]);
|
|
565
|
+
|
|
566
|
+
const result = await tool.execute("call", { question: "Explain?", options, allowCustomResponse: true }, new AbortController().signal, undefined, ctx);
|
|
567
|
+
|
|
568
|
+
assert.deepEqual(selectCalls[0]?.options, [options[0]!.label, options[1]!.label, "Other…"]);
|
|
569
|
+
assert.equal(inputCalls.length, 1);
|
|
570
|
+
assert.equal(inputCalls[0]?.title, "Explain?");
|
|
571
|
+
assert.deepEqual(result.details, { question: "Explain?", options, customResponse: "a free-form reply" });
|
|
572
|
+
assert.equal(result.content[0]?.text, "User responded: a free-form reply");
|
|
573
|
+
assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("ask_user_choice cancels through RPC dialogs like the TUI path when select returns undefined", async (t) => {
|
|
577
|
+
withInteractiveHostEnv(t);
|
|
578
|
+
const registration = registerChoiceTool();
|
|
579
|
+
const { tool } = registration;
|
|
580
|
+
const { ctx } = rpcHostContext([undefined]);
|
|
581
|
+
|
|
582
|
+
const result = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, ctx);
|
|
583
|
+
|
|
584
|
+
assert.deepEqual(result.details, { question: "Proceed?", options, cancelled: true });
|
|
585
|
+
assert.equal(result.content[0]?.text, "User cancelled the choice");
|
|
586
|
+
assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE, "the blocked lifecycle must still balance on a cancel");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("ask_user_choice cancels through RPC dialogs when the Other… input is cancelled", async (t) => {
|
|
590
|
+
withInteractiveHostEnv(t);
|
|
591
|
+
const registration = registerChoiceTool();
|
|
592
|
+
const { tool } = registration;
|
|
593
|
+
const { ctx } = rpcHostContext(["Other…"], [undefined]);
|
|
594
|
+
|
|
595
|
+
const result = await tool.execute("call", { question: "Explain?", options, allowCustomResponse: true }, new AbortController().signal, undefined, ctx);
|
|
596
|
+
|
|
597
|
+
assert.deepEqual(result.details, { question: "Explain?", options, cancelled: true });
|
|
598
|
+
assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE, "the blocked lifecycle must still balance on a cancel");
|
|
599
|
+
});
|
|
600
|
+
|
|
486
601
|
test("ask_user_choice emits a private balanced lifecycle around selection and cancellation", async () => {
|
|
487
602
|
const sequence: string[] = [];
|
|
488
603
|
const selectedRegistration = registerChoiceTool([], ({ data }) => {
|
|
@@ -582,3 +697,17 @@ test("ask_user_choice is offered only for interactive TUI turns and preserves th
|
|
|
582
697
|
for (const hook of registration.hooks) await hook({}, { mode: "print" });
|
|
583
698
|
assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
|
|
584
699
|
});
|
|
700
|
+
|
|
701
|
+
test("ask_user_choice is offered on an interactive rpc host and withdrawn once the variable is gone", async (t) => {
|
|
702
|
+
const registration = registerChoiceTool(["read"]);
|
|
703
|
+
registration.allowRuntimeActions();
|
|
704
|
+
t.after(() => { delete process.env[INTERACTIVE_HOST_ENV]; });
|
|
705
|
+
|
|
706
|
+
process.env[INTERACTIVE_HOST_ENV] = "1";
|
|
707
|
+
for (const hook of registration.hooks) await hook({}, { mode: "rpc" });
|
|
708
|
+
assert.deepEqual(registration.activeTools(), ["read", "ask_user_choice"]);
|
|
709
|
+
|
|
710
|
+
delete process.env[INTERACTIVE_HOST_ENV];
|
|
711
|
+
for (const hook of registration.hooks) await hook({}, { mode: "rpc" });
|
|
712
|
+
assert.deepEqual(registration.activeTools(), ["read"]);
|
|
713
|
+
});
|