mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
@@ -9,6 +9,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed extension OAuth login prompts to keep previous submitted prompt rows stable instead of mirroring the active input value ([#5433](https://github.com/earendil-works/pi/issues/5433)).
|
||||
- Fixed `/reload` to apply updated `steeringMode` and `followUpMode` settings to the current session ([#5377](https://github.com/earendil-works/pi/issues/5377)).
|
||||
- Fixed invalid `models.json` syntax to skip startup config migrations and report the normal file-path-aware models error instead of a raw JSON parse stack trace ([#5418](https://github.com/earendil-works/pi/issues/5418)).
|
||||
- Fixed GitHub release notes and interactive changelog links to resolve package-relative documentation URLs correctly ([#5516](https://github.com/earendil-works/pi/issues/5516)).
|
||||
|
||||
@@ -56,7 +56,9 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
this.input = new Input();
|
||||
this.input.onSubmit = () => {
|
||||
if (this.inputResolver) {
|
||||
this.inputResolver(this.input.getValue());
|
||||
const value = this.input.getValue();
|
||||
this.replaceInputWithSubmittedText(value);
|
||||
this.inputResolver(value);
|
||||
this.inputResolver = undefined;
|
||||
this.inputRejecter = undefined;
|
||||
}
|
||||
@@ -73,6 +75,12 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
return this.abortController.signal;
|
||||
}
|
||||
|
||||
private replaceInputWithSubmittedText(value: string): void {
|
||||
this.contentContainer.children = this.contentContainer.children.map((child) =>
|
||||
child === this.input ? new Text(`> ${value}`, 0, 0) : child,
|
||||
);
|
||||
}
|
||||
|
||||
private cancel(): void {
|
||||
this.abortController.abort();
|
||||
if (this.inputRejecter) {
|
||||
@@ -128,6 +136,7 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
* Show input for manual code/URL entry (for callback server providers)
|
||||
*/
|
||||
showManualInput(prompt: string): Promise<string> {
|
||||
this.input.setValue("");
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0));
|
||||
this.contentContainer.addChild(this.input);
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { setKeybindings, type TUI } from "@earendil-works/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { KeybindingsManager } from "../../../src/core/keybindings.ts";
|
||||
import { LoginDialogComponent } from "../../../src/modes/interactive/components/login-dialog.ts";
|
||||
import { initTheme } from "../../../src/modes/interactive/theme/theme.ts";
|
||||
import { stripAnsi } from "../../../src/utils/ansi.ts";
|
||||
|
||||
vi.mock("../../../src/utils/open-browser.ts", () => ({
|
||||
openBrowser: vi.fn(),
|
||||
}));
|
||||
|
||||
function createDialog(): LoginDialogComponent {
|
||||
return new LoginDialogComponent(
|
||||
{ requestRender: vi.fn() } as unknown as TUI,
|
||||
"prompt-repro",
|
||||
() => {},
|
||||
"Prompt Repro",
|
||||
);
|
||||
}
|
||||
|
||||
function renderDialog(dialog: LoginDialogComponent): string[] {
|
||||
return stripAnsi(dialog.render(120).join("\n"))
|
||||
.split("\n")
|
||||
.map((line) => line.trimEnd());
|
||||
}
|
||||
|
||||
function countRenderedValue(lines: string[], value: string): number {
|
||||
return lines.filter((line) => line.trim() === `> ${value}`).length;
|
||||
}
|
||||
|
||||
describe("LoginDialogComponent OAuth prompts", () => {
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
test("keeps previous prompt input stable when a later prompt is active", async () => {
|
||||
const dialog = createDialog();
|
||||
|
||||
const firstPrompt = dialog.showPrompt("First prompt:", "first-value");
|
||||
dialog.handleInput("first-value");
|
||||
dialog.handleInput("\n");
|
||||
await expect(firstPrompt).resolves.toBe("first-value");
|
||||
|
||||
const secondPrompt = dialog.showPrompt("Second prompt:");
|
||||
dialog.handleInput("second-secret-demo");
|
||||
|
||||
const lines = renderDialog(dialog);
|
||||
expect(lines.join("\n")).toContain("First prompt:");
|
||||
expect(lines.join("\n")).toContain("Second prompt:");
|
||||
expect(countRenderedValue(lines, "first-value")).toBe(1);
|
||||
expect(countRenderedValue(lines, "second-secret-demo")).toBe(1);
|
||||
|
||||
dialog.handleInput("\n");
|
||||
await expect(secondPrompt).resolves.toBe("second-secret-demo");
|
||||
});
|
||||
|
||||
test("preserves auth instructions when showing a prompt", () => {
|
||||
const dialog = createDialog();
|
||||
|
||||
dialog.showAuth("https://example.invalid/login", "Authorize the extension");
|
||||
dialog.showPrompt("First prompt:");
|
||||
|
||||
const output = renderDialog(dialog).join("\n");
|
||||
expect(output).toContain("https://example.invalid/login");
|
||||
expect(output).toContain("Authorize the extension");
|
||||
expect(output).toContain("First prompt:");
|
||||
});
|
||||
|
||||
test("keeps previous manual input stable when a later prompt is active", async () => {
|
||||
const dialog = createDialog();
|
||||
|
||||
const manualInput = dialog.showManualInput("Paste callback URL:");
|
||||
dialog.handleInput("callback-value");
|
||||
dialog.handleInput("\n");
|
||||
await expect(manualInput).resolves.toBe("callback-value");
|
||||
|
||||
const prompt = dialog.showPrompt("Second prompt:");
|
||||
dialog.handleInput("second-secret-demo");
|
||||
|
||||
const lines = renderDialog(dialog);
|
||||
expect(lines.join("\n")).toContain("Paste callback URL:");
|
||||
expect(lines.join("\n")).toContain("Second prompt:");
|
||||
expect(countRenderedValue(lines, "callback-value")).toBe(1);
|
||||
expect(countRenderedValue(lines, "second-secret-demo")).toBe(1);
|
||||
|
||||
dialog.handleInput("\n");
|
||||
await expect(prompt).resolves.toBe("second-secret-demo");
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { defineConfig } from "vitest/config";
|
||||
const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url));
|
||||
const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url));
|
||||
const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url));
|
||||
const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@@ -21,9 +22,11 @@ export default defineConfig({
|
||||
{ find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex },
|
||||
{ find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth },
|
||||
{ find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex },
|
||||
{ find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex },
|
||||
{ find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex },
|
||||
{ find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth },
|
||||
{ find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex },
|
||||
{ find: /^@mariozechner\/pi-tui$/, replacement: tuiSrcIndex },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user