gentle-pi 4.3.0 → 4.4.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 +8 -5
- package/assets/agents/gentle-ai-worker.md +1 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +101 -151
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell.md +77 -7
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +19 -15
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +173 -38
- package/extensions/gentle-ai.ts +214 -69
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +332 -73
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +123 -49
- package/lib/gentle-shell-launcher.ts +9 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +4 -3
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +122 -48
- package/runtime/gentle-shell-launcher.mjs +9 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +34 -6
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +226 -5
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +20 -0
- package/tests/bundled-install.test.ts +3 -3
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +1 -1
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +365 -20
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bundled.test.ts +301 -248
- package/tests/gentle-shell-launcher.test.ts +28 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +183 -8
- package/tests/gentle-todo.test.ts +11 -0
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +99 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +14 -2
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
package/tests/gentle-ai.test.ts
CHANGED
|
@@ -526,6 +526,7 @@ test("models saves and clears independent provider review roles without local ar
|
|
|
526
526
|
}
|
|
527
527
|
assert.match(initial, /Pi persisted default model/);
|
|
528
528
|
assert.match(initial, /Pi persisted default effort/);
|
|
529
|
+
panel.handleInput("j"); // Skip the snapshot-only orchestrator row.
|
|
529
530
|
for (const [index, model] of ["alpha", "beta"].entries()) {
|
|
530
531
|
panel.handleInput("j");
|
|
531
532
|
panel.handleInput("\r");
|
|
@@ -549,6 +550,7 @@ test("models saves and clears independent provider review roles without local ar
|
|
|
549
550
|
assertNoLocalArtifacts();
|
|
550
551
|
|
|
551
552
|
fixture.onInput((panel) => {
|
|
553
|
+
panel.handleInput("j");
|
|
552
554
|
panel.handleInput("j");
|
|
553
555
|
panel.handleInput("i");
|
|
554
556
|
panel.handleInput("\x13");
|
|
@@ -561,6 +563,7 @@ test("models saves and clears independent provider review roles without local ar
|
|
|
561
563
|
assertNoLocalArtifacts();
|
|
562
564
|
|
|
563
565
|
fixture.onInput((panel) => {
|
|
566
|
+
panel.handleInput("j");
|
|
564
567
|
panel.handleInput("j");
|
|
565
568
|
panel.handleInput("j");
|
|
566
569
|
panel.handleInput("i");
|
|
@@ -1297,8 +1300,8 @@ test("model panel lists grow with the terminal height instead of a fixed window"
|
|
|
1297
1300
|
.renderSddModelPanel({}, models, agents, 100, undefined, 40)
|
|
1298
1301
|
.map(stripAnsi);
|
|
1299
1302
|
assert.equal(agentLines.length, 40);
|
|
1300
|
-
// 40 rows minus 15 rows of chrome:
|
|
1301
|
-
assert.equal(agentLines.filter((line) => /(agent-\d\d|Set all agents)\s+model
|
|
1303
|
+
// 40 rows minus 15 rows of chrome: agents, "Set all", and the orchestrator share the space.
|
|
1304
|
+
assert.equal(agentLines.filter((line) => /(agent-\d\d|Set all agents)\s+model=|orchestrator\s+leave untouched/.test(line)).length, 25);
|
|
1302
1305
|
assert.ok(agentLines.some((line) => /x export/.test(line)));
|
|
1303
1306
|
|
|
1304
1307
|
const pickerLines = __testing
|
|
@@ -2933,11 +2936,186 @@ test("s snapshots the live session's orchestrator over the settings defaults", a
|
|
|
2933
2936
|
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore, "settings are untouched");
|
|
2934
2937
|
});
|
|
2935
2938
|
|
|
2939
|
+
function chooseOrchestrator(panel: RoutingConsumerPanel): void {
|
|
2940
|
+
panel.handleInput("g");
|
|
2941
|
+
panel.handleInput("j");
|
|
2942
|
+
assert.match(renderComponent(panel), /▸ orchestrator/);
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
function chooseModel(panel: RoutingConsumerPanel, query: string): void {
|
|
2946
|
+
panel.handleInput("\r");
|
|
2947
|
+
for (const character of query) panel.handleInput(character);
|
|
2948
|
+
panel.handleInput("\r");
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function chooseEffort(panel: RoutingConsumerPanel, effort: string): void {
|
|
2952
|
+
panel.handleInput("e");
|
|
2953
|
+
for (let step = 0; step < 10 && !renderComponent(panel).includes(`▸ ${effort}`); step++) panel.handleInput("j");
|
|
2954
|
+
assert.ok(renderComponent(panel).includes(`▸ ${effort}`));
|
|
2955
|
+
panel.handleInput("\r");
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
test("models explicit orchestrator choice wins over live/defaults and agent bulk edits", async (t) => {
|
|
2959
|
+
const { fixture, storePath, settingsPath, writeStore, writeSettings } = profilesStoreFixture(t);
|
|
2960
|
+
// Enter in /gentle:profiles binds the fixture session in a process-wide store.
|
|
2961
|
+
t.after(() => resetSessionProfileBindingsForTesting());
|
|
2962
|
+
writeSettings();
|
|
2963
|
+
const settingsBefore = readFileSync(settingsPath, "utf8");
|
|
2964
|
+
fixture.setLiveModel("openai", "alpha", "high");
|
|
2965
|
+
writeStore({ team: { orchestrator: { model: "nan/glm5.3", thinking: "max" } } }, "team");
|
|
2966
|
+
fixture.onInput((panel) => {
|
|
2967
|
+
assert.match(renderComponent(panel), /orchestrator\s+model=nan\/glm5.3, effort=max/);
|
|
2968
|
+
chooseOrchestrator(panel);
|
|
2969
|
+
chooseModel(panel, "beta");
|
|
2970
|
+
chooseEffort(panel, "low");
|
|
2971
|
+
panel.handleInput("g");
|
|
2972
|
+
chooseModel(panel, "alpha");
|
|
2973
|
+
chooseEffort(panel, "high");
|
|
2974
|
+
assert.match(renderComponent(panel), /orchestrator\s+model=openai\/beta, effort=low/);
|
|
2975
|
+
panel.handleInput("u");
|
|
2976
|
+
});
|
|
2977
|
+
await fixture.run("gentle:models");
|
|
2978
|
+
const snapshot = JSON.parse(readFileSync(storePath, "utf8")).profiles.team;
|
|
2979
|
+
assert.deepEqual(snapshot.orchestrator, { model: "openai/beta", thinking: "low" });
|
|
2980
|
+
assert.deepEqual(snapshot.worker, { model: "openai/alpha", thinking: "high" });
|
|
2981
|
+
assert.equal("orchestrator" in JSON.parse(readFileSync(fixture.globalPath, "utf8")), false);
|
|
2982
|
+
assert.equal("orchestrator" in JSON.parse(readFileSync(join(fixture.root, ".pi", "subagents.json"), "utf8")).model_profiles, false);
|
|
2983
|
+
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore);
|
|
2984
|
+
assert.deepEqual(fixture.liveSwitches, [], "saving never switches the live model or effort");
|
|
2985
|
+
assert.equal(fixture.notifications.some((entry) => entry.severity === "warning"), false);
|
|
2986
|
+
let profileVisits = 0;
|
|
2987
|
+
fixture.onInput((panel) => { panel.handleInput(profileVisits++ === 0 ? "\r" : "\x1b"); });
|
|
2988
|
+
await fixture.run("gentle:profiles");
|
|
2989
|
+
assert.deepEqual(fixture.liveSwitches, [
|
|
2990
|
+
{ kind: "model", provider: "openai", id: "beta" },
|
|
2991
|
+
{ kind: "thinking", level: "low" },
|
|
2992
|
+
]);
|
|
2993
|
+
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore);
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2996
|
+
test("models leave untouched removes stale orchestrator and preserves live model/effort on apply", async (t) => {
|
|
2997
|
+
const { fixture, storePath, settingsPath, writeStore, writeSettings } = profilesStoreFixture(t);
|
|
2998
|
+
// Enter in /gentle:profiles binds the fixture session in a process-wide store.
|
|
2999
|
+
t.after(() => resetSessionProfileBindingsForTesting());
|
|
3000
|
+
writeSettings();
|
|
3001
|
+
const settingsBefore = readFileSync(settingsPath, "utf8");
|
|
3002
|
+
fixture.setLiveModel("openai", "alpha", "low");
|
|
3003
|
+
writeStore({ team: { orchestrator: { model: "openai/beta", thinking: "high" } } }, "team");
|
|
3004
|
+
writeFileSync(fixture.globalPath, JSON.stringify({ orchestrator: { model: "openai/beta", thinking: "high" }, worker: { model: "openai/alpha" } }));
|
|
3005
|
+
fixture.answerInputs("");
|
|
3006
|
+
let visits = 0;
|
|
3007
|
+
fixture.onInput((panel) => {
|
|
3008
|
+
if (visits++ === 0) {
|
|
3009
|
+
chooseOrchestrator(panel);
|
|
3010
|
+
chooseModel(panel, "leave untouched");
|
|
3011
|
+
panel.handleInput("e");
|
|
3012
|
+
assert.match(renderComponent(panel), /orchestrator\s+leave untouched/);
|
|
3013
|
+
panel.handleInput("j");
|
|
3014
|
+
panel.handleInput("c");
|
|
3015
|
+
} else {
|
|
3016
|
+
assert.match(renderComponent(panel), /orchestrator\s+leave untouched/);
|
|
3017
|
+
panel.handleInput("u");
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
await fixture.run("gentle:models");
|
|
3021
|
+
assert.equal(Object.hasOwn(JSON.parse(readFileSync(storePath, "utf8")).profiles.team, "orchestrator"), false);
|
|
3022
|
+
assert.equal(Object.hasOwn(JSON.parse(readFileSync(fixture.globalPath, "utf8")), "orchestrator"), false);
|
|
3023
|
+
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore);
|
|
3024
|
+
assert.deepEqual(fixture.liveSwitches, []);
|
|
3025
|
+
let profileVisits = 0;
|
|
3026
|
+
fixture.onInput((panel) => {
|
|
3027
|
+
assert.match(renderComponent(panel), /orchestrator\s+inherit/);
|
|
3028
|
+
panel.handleInput(profileVisits++ === 0 ? "\r" : "\x1b");
|
|
3029
|
+
});
|
|
3030
|
+
await fixture.run("gentle:profiles");
|
|
3031
|
+
assert.deepEqual(fixture.liveSwitches, [], "Enter preserves both live model and effort without the key");
|
|
3032
|
+
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore);
|
|
3033
|
+
});
|
|
3034
|
+
|
|
3035
|
+
for (const [name, key] of [["ctrl+s", "\x13"], ["cancel", "\x1b"]]) {
|
|
3036
|
+
test(`models ${name} never snapshots or applies the chosen orchestrator`, async (t) => {
|
|
3037
|
+
const { fixture, storePath, settingsPath, writeStore, writeSettings } = profilesStoreFixture(t);
|
|
3038
|
+
writeSettings();
|
|
3039
|
+
fixture.setLiveModel("openai", "alpha", "high");
|
|
3040
|
+
writeStore({ team: { orchestrator: { model: "openai/alpha", thinking: "high" } } }, "team");
|
|
3041
|
+
const before = [readFileSync(storePath, "utf8"), readFileSync(settingsPath, "utf8")];
|
|
3042
|
+
fixture.onInput((panel) => {
|
|
3043
|
+
chooseOrchestrator(panel);
|
|
3044
|
+
chooseModel(panel, "beta");
|
|
3045
|
+
chooseEffort(panel, "low");
|
|
3046
|
+
panel.handleInput(key!);
|
|
3047
|
+
});
|
|
3048
|
+
await fixture.run("gentle:models");
|
|
3049
|
+
assert.deepEqual([readFileSync(storePath, "utf8"), readFileSync(settingsPath, "utf8")], before);
|
|
3050
|
+
assert.deepEqual(fixture.liveSwitches, []);
|
|
3051
|
+
if (name === "ctrl+s") assert.deepEqual(JSON.parse(readFileSync(fixture.globalPath, "utf8")), {});
|
|
3052
|
+
else assert.equal(existsSync(fixture.globalPath), false);
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
for (const action of ["bulk inherit", "custom", "export/restore"]) {
|
|
3057
|
+
for (const leaveUntouched of [false, true]) {
|
|
3058
|
+
test(`models choice survives ${action} with ${leaveUntouched ? "no key" : "explicit pair"}`, async (t) => {
|
|
3059
|
+
const { fixture, storePath, settingsPath, writeStore, writeSettings } = profilesStoreFixture(t);
|
|
3060
|
+
writeSettings();
|
|
3061
|
+
fixture.setLiveModel("openai", "alpha", "high");
|
|
3062
|
+
writeStore({ team: { orchestrator: { model: "openai/alpha", thinking: "high" } } }, "team");
|
|
3063
|
+
writeFileSync(fixture.globalPath, JSON.stringify({ worker: { model: "openai/alpha" } }));
|
|
3064
|
+
const settingsBefore = readFileSync(settingsPath, "utf8");
|
|
3065
|
+
if (action === "custom") fixture.answerInputs("openai/alpha");
|
|
3066
|
+
let visits = 0;
|
|
3067
|
+
const assertChoice = (panel: RoutingConsumerPanel) => {
|
|
3068
|
+
assert.match(renderComponent(panel), leaveUntouched
|
|
3069
|
+
? /orchestrator\s+leave untouched/
|
|
3070
|
+
: /orchestrator\s+model=openai\/beta, effort=low/);
|
|
3071
|
+
};
|
|
3072
|
+
fixture.onInput((panel) => {
|
|
3073
|
+
visits++;
|
|
3074
|
+
assert.ok(visits <= 3, "reopens must be bounded");
|
|
3075
|
+
if (visits === 1) {
|
|
3076
|
+
chooseOrchestrator(panel);
|
|
3077
|
+
if (leaveUntouched) panel.handleInput("i");
|
|
3078
|
+
else {
|
|
3079
|
+
chooseModel(panel, "beta");
|
|
3080
|
+
chooseEffort(panel, "low");
|
|
3081
|
+
}
|
|
3082
|
+
assertChoice(panel);
|
|
3083
|
+
if (action === "bulk inherit") {
|
|
3084
|
+
panel.handleInput("g");
|
|
3085
|
+
panel.handleInput("i");
|
|
3086
|
+
assertChoice(panel);
|
|
3087
|
+
panel.handleInput("u");
|
|
3088
|
+
} else if (action === "custom") {
|
|
3089
|
+
panel.handleInput("g");
|
|
3090
|
+
for (let step = 0; step < 4; step++) panel.handleInput("j");
|
|
3091
|
+
assert.match(renderComponent(panel), /▸ worker/);
|
|
3092
|
+
panel.handleInput("c");
|
|
3093
|
+
} else panel.handleInput("x");
|
|
3094
|
+
} else {
|
|
3095
|
+
assertChoice(panel);
|
|
3096
|
+
panel.handleInput(action === "export/restore" && visits === 2 ? "r" : "u");
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
await fixture.run("gentle:models");
|
|
3100
|
+
const profile = JSON.parse(readFileSync(storePath, "utf8")).profiles.team;
|
|
3101
|
+
assert.equal(Object.hasOwn(profile, "orchestrator"), !leaveUntouched);
|
|
3102
|
+
if (!leaveUntouched) assert.deepEqual(profile.orchestrator, { model: "openai/beta", thinking: "low" });
|
|
3103
|
+
assert.deepEqual(profile.worker, action === "bulk inherit" ? {} : { model: "openai/alpha" });
|
|
3104
|
+
assert.equal(Object.hasOwn(JSON.parse(readFileSync(fixture.globalPath, "utf8")), "orchestrator"), false);
|
|
3105
|
+
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore);
|
|
3106
|
+
assert.deepEqual(fixture.liveSwitches, []);
|
|
3107
|
+
assert.equal(fixture.notifications.some((entry) => entry.severity === "warning"), false);
|
|
3108
|
+
assert.equal(visits, action === "bulk inherit" ? 1 : action === "custom" ? 2 : 3);
|
|
3109
|
+
assert.equal(fixture.confirmCalls.length, action === "export/restore" ? 1 : 0);
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
|
|
2936
3114
|
// /gentle:models can finish with `u`: the global save `ctrl+s` performs, followed
|
|
2937
|
-
// by
|
|
3115
|
+
// by a snapshot of saved agent routing and the explicitly chosen orchestrator.
|
|
2938
3116
|
function pickWorkerModelThenUpdateProfile(panel: RoutingConsumerPanel): void {
|
|
2939
|
-
// Rows: "Set all agents",
|
|
2940
|
-
for (let step = 0; step <
|
|
3117
|
+
// Rows: "Set all agents", "orchestrator", two provider review roles, then "worker".
|
|
3118
|
+
for (let step = 0; step < 4; step += 1) panel.handleInput("j");
|
|
2941
3119
|
assert.match(renderComponent(panel), /▸ worker/);
|
|
2942
3120
|
panel.handleInput("\r");
|
|
2943
3121
|
for (const character of "alpha") panel.handleInput(character);
|
|
@@ -2945,7 +3123,7 @@ function pickWorkerModelThenUpdateProfile(panel: RoutingConsumerPanel): void {
|
|
|
2945
3123
|
panel.handleInput("u");
|
|
2946
3124
|
}
|
|
2947
3125
|
|
|
2948
|
-
test("u
|
|
3126
|
+
test("u preserves an active profile's absent orchestrator when editing agents", async (t) => {
|
|
2949
3127
|
const { fixture, storePath, settingsPath, writeStore, writeSettings } = profilesStoreFixture(t);
|
|
2950
3128
|
writeSettings();
|
|
2951
3129
|
const settingsBefore = readFileSync(settingsPath, "utf8");
|
|
@@ -2956,7 +3134,8 @@ test("u saves global routing and captures live session orchestrator instead of d
|
|
|
2956
3134
|
}, "team");
|
|
2957
3135
|
fixture.onInput((panel) => {
|
|
2958
3136
|
assert.match(renderComponent(panel), /Current profile: team/);
|
|
2959
|
-
assert.match(renderComponent(panel), /
|
|
3137
|
+
assert.match(renderComponent(panel), /orchestrator\s+leave untouched/);
|
|
3138
|
+
assert.match(renderComponent(panel), /u save choice in "team"/);
|
|
2960
3139
|
pickWorkerModelThenUpdateProfile(panel);
|
|
2961
3140
|
});
|
|
2962
3141
|
await fixture.run("gentle:models");
|
|
@@ -2965,8 +3144,8 @@ test("u saves global routing and captures live session orchestrator instead of d
|
|
|
2965
3144
|
const store = JSON.parse(readFileSync(storePath, "utf8"));
|
|
2966
3145
|
assert.deepEqual(store.profiles.team, {
|
|
2967
3146
|
worker: { model: "openai/alpha" },
|
|
2968
|
-
orchestrator: { model: "openai/alpha", thinking: "low" },
|
|
2969
3147
|
});
|
|
3148
|
+
assert.deepEqual(fixture.liveSwitches, []);
|
|
2970
3149
|
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore, "global defaults remain unchanged");
|
|
2971
3150
|
assert.deepEqual(store.profiles.other, { worker: { model: "openai/beta", thinking: "high" } });
|
|
2972
3151
|
assert.equal(store.active, "team");
|
|
@@ -2981,17 +3160,19 @@ test("u saves global routing and captures live session orchestrator instead of d
|
|
|
2981
3160
|
);
|
|
2982
3161
|
});
|
|
2983
3162
|
|
|
2984
|
-
test("u
|
|
2985
|
-
const { fixture, storePath, writeStore, writeSettings, writePin, localPinPath } = profilesStoreFixture(t);
|
|
3163
|
+
test("u preserves the pinned profile's stored orchestrator instead of live or active values", async (t) => {
|
|
3164
|
+
const { fixture, storePath, settingsPath, writeStore, writeSettings, writePin, localPinPath } = profilesStoreFixture(t);
|
|
2986
3165
|
writeSettings();
|
|
2987
|
-
|
|
3166
|
+
const settingsBefore = readFileSync(settingsPath, "utf8");
|
|
3167
|
+
fixture.setLiveModel("openai", "alpha", "low");
|
|
2988
3168
|
writeStore({
|
|
2989
3169
|
team: { worker: { model: "openai/beta" } },
|
|
2990
|
-
other: { worker: { model: "openai/beta", thinking: "high" } },
|
|
3170
|
+
other: { worker: { model: "openai/beta", thinking: "high" }, orchestrator: { model: "openai/beta", thinking: "high" } },
|
|
2991
3171
|
}, "team");
|
|
2992
3172
|
writePin(localPinPath, "other");
|
|
2993
3173
|
fixture.onInput((panel) => {
|
|
2994
3174
|
assert.match(renderComponent(panel), /Current profile: other \(pinned\)/);
|
|
3175
|
+
assert.match(renderComponent(panel), /orchestrator\s+model=openai\/beta, effort=high/);
|
|
2995
3176
|
pickWorkerModelThenUpdateProfile(panel);
|
|
2996
3177
|
});
|
|
2997
3178
|
await fixture.run("gentle:models");
|
|
@@ -3000,8 +3181,10 @@ test("u updates the pinned profile instead of the active one inside a pinned rep
|
|
|
3000
3181
|
const store = JSON.parse(readFileSync(storePath, "utf8"));
|
|
3001
3182
|
assert.deepEqual(store.profiles.other, {
|
|
3002
3183
|
worker: { model: "openai/alpha" },
|
|
3003
|
-
orchestrator: { model: "openai/beta", thinking: "
|
|
3184
|
+
orchestrator: { model: "openai/beta", thinking: "high" },
|
|
3004
3185
|
});
|
|
3186
|
+
assert.deepEqual(fixture.liveSwitches, []);
|
|
3187
|
+
assert.equal(readFileSync(settingsPath, "utf8"), settingsBefore);
|
|
3005
3188
|
assert.deepEqual(store.profiles.team, { worker: { model: "openai/beta" } });
|
|
3006
3189
|
assert.equal(store.active, "team");
|
|
3007
3190
|
assert.ok(
|
|
@@ -3013,10 +3196,12 @@ test("u updates the pinned profile instead of the active one inside a pinned rep
|
|
|
3013
3196
|
test("u keeps the global save and reports when no profile is current", async (t) => {
|
|
3014
3197
|
const { fixture, storePath, writeStore, writeSettings } = profilesStoreFixture(t);
|
|
3015
3198
|
writeSettings();
|
|
3199
|
+
fixture.setLiveModel("openai", "alpha", "low");
|
|
3016
3200
|
writeStore({ team: { worker: { model: "openai/beta" } } });
|
|
3017
3201
|
const before = readFileSync(storePath, "utf8");
|
|
3018
3202
|
fixture.onInput((panel) => {
|
|
3019
3203
|
assert.match(renderComponent(panel), /Current profile: none/);
|
|
3204
|
+
assert.match(renderComponent(panel), /orchestrator\s+model=openai\/alpha, effort=low/);
|
|
3020
3205
|
pickWorkerModelThenUpdateProfile(panel);
|
|
3021
3206
|
});
|
|
3022
3207
|
await fixture.run("gentle:models");
|
|
@@ -3035,6 +3220,7 @@ test("u seeds the profiles store the way /gentle:profiles does when it is missin
|
|
|
3035
3220
|
assert.equal(existsSync(storePath), false);
|
|
3036
3221
|
fixture.onInput((panel) => {
|
|
3037
3222
|
assert.match(renderComponent(panel), /Current profile: none/);
|
|
3223
|
+
assert.match(renderComponent(panel), /orchestrator\s+model=nan\/deepseek-v4-flash, effort=high/);
|
|
3038
3224
|
pickWorkerModelThenUpdateProfile(panel);
|
|
3039
3225
|
});
|
|
3040
3226
|
await fixture.run("gentle:models");
|
|
@@ -3141,6 +3327,7 @@ test("the profiles panel fills the terminal, lists routing per agent, and scroll
|
|
|
3141
3327
|
orchestrator: { model: "nan/glm5.3", thinking: "high" },
|
|
3142
3328
|
worker: { model: "openai/alpha", thinking: "high" },
|
|
3143
3329
|
"sdd-design": { model: "nan/glm5.3", thinking: "high" },
|
|
3330
|
+
"review-refuter": { model: "nan/glm5.3", thinking: "high" },
|
|
3144
3331
|
},
|
|
3145
3332
|
}, "team");
|
|
3146
3333
|
let rendered: string | undefined;
|
|
@@ -3165,6 +3352,15 @@ test("the profiles panel fills the terminal, lists routing per agent, and scroll
|
|
|
3165
3352
|
assert.match(text, /Current routing \(effective\)/);
|
|
3166
3353
|
assert.match(text, /orchestrator\s+nan\/glm5\.3 · high/);
|
|
3167
3354
|
assert.match(text, /worker\s+openai\/alpha\s+high/);
|
|
3355
|
+
assert.match(text, /sdd-design\s+nan\/glm5\.3\s+high\s+← no matching agent/);
|
|
3356
|
+
// A matched key stays a plain row: the note is reserved for keys that name
|
|
3357
|
+
// no discovered agent (issue #2196).
|
|
3358
|
+
assert.doesNotMatch(text, /worker\s+openai\/alpha\s+high\s+←/);
|
|
3359
|
+
// gentle-pi#2271: review-refuter and review-validator are provider review
|
|
3360
|
+
// roles the relay routes by key; they never have an agent file, so the
|
|
3361
|
+
// panel must not flag them as unmatched.
|
|
3362
|
+
assert.match(text, /review-refuter\s+nan\/glm5\.3\s+high/);
|
|
3363
|
+
assert.doesNotMatch(text, /review-refuter[^\n]*(←|↳)/);
|
|
3168
3364
|
assert.match(text, /sdd-design\s+nan\/glm5\.3\s+high/);
|
|
3169
3365
|
assert.doesNotMatch(text, /agents? → /);
|
|
3170
3366
|
|
|
@@ -3182,6 +3378,89 @@ test("the profiles panel fills the terminal, lists routing per agent, and scroll
|
|
|
3182
3378
|
assert.match(shortLines.at(-1)!, /^╰/);
|
|
3183
3379
|
});
|
|
3184
3380
|
|
|
3381
|
+
test("a narrow detail pane keeps the unmatched-key note visible", async (t) => {
|
|
3382
|
+
const { fixture, writeStore } = profilesStoreFixture(t);
|
|
3383
|
+
writeStore({
|
|
3384
|
+
team: {
|
|
3385
|
+
worker: { model: "openai/alpha", thinking: "high" },
|
|
3386
|
+
"sdd-design": { model: "nan/glm5.3", thinking: "high" },
|
|
3387
|
+
},
|
|
3388
|
+
}, "team");
|
|
3389
|
+
let panel: { render(width: number): string[] } | undefined;
|
|
3390
|
+
fixture.onInput((visited) => {
|
|
3391
|
+
panel = visited;
|
|
3392
|
+
visited.handleInput("\x1b");
|
|
3393
|
+
});
|
|
3394
|
+
fixture.tui.terminal.rows = 40;
|
|
3395
|
+
await fixture.run("gentle:profiles");
|
|
3396
|
+
|
|
3397
|
+
assert.ok(panel);
|
|
3398
|
+
// At 60 columns the routing row fills the detail pane, so the note wraps
|
|
3399
|
+
// onto its own line instead of being truncated away (issue #2196).
|
|
3400
|
+
const narrow = stripAnsi(panel.render(60).join("\n"));
|
|
3401
|
+
assert.match(narrow, /↳ no matching agent/);
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
// Issue #2400: a model_profiles key that matches no discovered agent and has
|
|
3405
|
+
// no suggestion never became a "Current routing (effective)" row, because that
|
|
3406
|
+
// table is built per discovered agent. The panel now lists those keys under
|
|
3407
|
+
// their own heading; a key with a suggestion is left to the launch warning.
|
|
3408
|
+
test("the profiles panel lists model_profiles keys that match no agent and suggest none", async (t) => {
|
|
3409
|
+
const { fixture, writeStore } = profilesStoreFixture(t);
|
|
3410
|
+
writeStore({ team: { worker: { model: "openai/alpha", thinking: "high" } } }, "team");
|
|
3411
|
+
writeFileSync(join(fixture.root, ".pi", "subagents.json"), `${JSON.stringify({
|
|
3412
|
+
model_profiles: {
|
|
3413
|
+
worker: { model: "openai/alpha" },
|
|
3414
|
+
explor: { model: "openai/beta" },
|
|
3415
|
+
"sdd-orchestratr-x": { model: "openai/beta" },
|
|
3416
|
+
"gentle-ai-worker": { model: "openai/beta" },
|
|
3417
|
+
},
|
|
3418
|
+
}, null, 2)}\n`);
|
|
3419
|
+
let panel: { render(width: number): string[] } | undefined;
|
|
3420
|
+
fixture.onInput((visited) => {
|
|
3421
|
+
panel = visited;
|
|
3422
|
+
visited.handleInput("\x1b");
|
|
3423
|
+
});
|
|
3424
|
+
fixture.tui.terminal.rows = 40;
|
|
3425
|
+
await fixture.run("gentle:profiles");
|
|
3426
|
+
|
|
3427
|
+
assert.ok(panel);
|
|
3428
|
+
const wide = stripAnsi(panel.render(120).join("\n"));
|
|
3429
|
+
assert.match(wide, /Unmatched keys \(ignored\)/);
|
|
3430
|
+
assert.match(wide, /explor\s+← no matching agent/);
|
|
3431
|
+
assert.match(wide, /sdd-orchestratr-x\s+← no matching agent/);
|
|
3432
|
+
assert.doesNotMatch(wide, /gentle-ai-worker/, "a key with a suggestion is not listed as unmatched");
|
|
3433
|
+
|
|
3434
|
+
// At 60 columns the detail pane is 32 wide: the heading and the key stay
|
|
3435
|
+
// whole, and a note that does not fit moves to its own line.
|
|
3436
|
+
const narrow = stripAnsi(panel.render(60).join("\n"));
|
|
3437
|
+
assert.match(narrow, /Unmatched keys \(ignored\)/);
|
|
3438
|
+
assert.match(narrow, /explor\s+← no matching agent/);
|
|
3439
|
+
assert.match(narrow, /sdd-orchestratr-x[^\n]*\n[^\n]*↳ no matching agent/);
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
// Issue #2400: when a pin wins, launches route from the pinned snapshot, whose
|
|
3443
|
+
// keys are already current rows with their own notes, so the subagents.json
|
|
3444
|
+
// keys are not listed as the routing in effect.
|
|
3445
|
+
test("the unmatched-keys list stays out of the panel while a pin wins", async (t) => {
|
|
3446
|
+
const { fixture, writeStore, writePin, localPinPath } = profilesStoreFixture(t);
|
|
3447
|
+
writeStore({ team: { worker: { model: "openai/alpha", thinking: "high" } } }, "team");
|
|
3448
|
+
writePin(localPinPath, "team");
|
|
3449
|
+
writeFileSync(join(fixture.root, ".pi", "subagents.json"), `${JSON.stringify({ model_profiles: { explor: { model: "openai/beta" } } }, null, 2)}\n`);
|
|
3450
|
+
let panel: { render(width: number): string[] } | undefined;
|
|
3451
|
+
fixture.onInput((visited) => {
|
|
3452
|
+
panel = visited;
|
|
3453
|
+
visited.handleInput("\x1b");
|
|
3454
|
+
});
|
|
3455
|
+
fixture.tui.terminal.rows = 40;
|
|
3456
|
+
await fixture.run("gentle:profiles");
|
|
3457
|
+
|
|
3458
|
+
assert.ok(panel);
|
|
3459
|
+
const text = stripAnsi(panel.render(120).join("\n"));
|
|
3460
|
+
assert.match(text, /Current routing \(effective\)/);
|
|
3461
|
+
assert.doesNotMatch(text, /Unmatched keys/);
|
|
3462
|
+
});
|
|
3463
|
+
|
|
3185
3464
|
test("j and k scroll the detail pane one line at a time, like the agents view", async (t) => {
|
|
3186
3465
|
const { fixture, writeStore } = profilesStoreFixture(t);
|
|
3187
3466
|
const routing: Record<string, { model: string; thinking: string }> = {};
|