mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): support Windows fullscreen right-click paste
This commit is contained in:
parent
7d046bcf21
commit
c96bfaccd0
6 changed files with 93 additions and 1 deletions
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
- Added `pi auth check` provider/model auth preflight with optional credential output ([#7152](https://github.com/earendil-works/pi/issues/7152)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed right-click not pasting clipboard text in fullscreen mode on Windows.
|
||||
|
||||
## [0.84.0] - 2026-08-06
|
||||
|
||||
### New Features
|
||||
|
|
|
|||
|
|
@ -336,13 +336,17 @@ interface InteractiveTuiOptions {
|
|||
showHardwareCursor: boolean;
|
||||
logDirectory: string;
|
||||
terminal?: Terminal;
|
||||
onRightClickPaste?: () => void;
|
||||
}
|
||||
|
||||
/** Composition root for selecting the interactive terminal renderer. */
|
||||
export function createInteractiveTui(options: InteractiveTuiOptions): TuiMainScreen | TuiAltScreen {
|
||||
const terminal = options.terminal ?? new ProcessTerminal();
|
||||
if (options.tuiMode === "fullscreen") {
|
||||
return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, { openUrl: openBrowser });
|
||||
return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, {
|
||||
openUrl: openBrowser,
|
||||
onRightClickPaste: options.onRightClickPaste,
|
||||
});
|
||||
}
|
||||
return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
|
||||
}
|
||||
|
|
@ -493,6 +497,9 @@ export class InteractiveMode {
|
|||
private customHeader: (Component & { dispose?(): void }) | undefined = undefined;
|
||||
|
||||
private options: InteractiveModeOptions;
|
||||
private readonly onRightClickPaste = (): void => {
|
||||
void this.handleRightClickPaste();
|
||||
};
|
||||
private autoTrustOnReloadCwd: string | undefined;
|
||||
private themeController: InteractiveThemeController;
|
||||
|
||||
|
|
@ -526,6 +533,7 @@ export class InteractiveMode {
|
|||
tuiMode,
|
||||
showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
|
||||
logDirectory: getAgentDir(),
|
||||
onRightClickPaste: this.onRightClickPaste,
|
||||
});
|
||||
this.ui = createInteractiveTuiReference(() => this.renderer);
|
||||
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
||||
|
|
@ -794,6 +802,7 @@ export class InteractiveMode {
|
|||
showHardwareCursor,
|
||||
logDirectory: getAgentDir(),
|
||||
terminal,
|
||||
onRightClickPaste: this.onRightClickPaste,
|
||||
});
|
||||
nextUi.setClearOnShrink(clearOnShrink);
|
||||
nextUi.onDebug = onDebug;
|
||||
|
|
@ -2811,6 +2820,20 @@ export class InteractiveMode {
|
|||
};
|
||||
}
|
||||
|
||||
private async handleRightClickPaste(): Promise<void> {
|
||||
const target = this.renderer.getFocusedComponent();
|
||||
const handleInput = target?.handleInput;
|
||||
if (!target || !handleInput) return;
|
||||
try {
|
||||
const text = await readClipboardText();
|
||||
if (!text || this.renderer.getFocusedComponent() !== target) return;
|
||||
handleInput.call(target, `\x1b[200~${text}\x1b[201~`);
|
||||
this.ui.requestRender();
|
||||
} catch {
|
||||
// Silently ignore clipboard errors (may not have permission, etc.)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleClipboardPaste(): Promise<void> {
|
||||
try {
|
||||
const image = await readClipboardImage();
|
||||
|
|
|
|||
|
|
@ -128,6 +128,27 @@ describe("createInteractiveTui", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("InteractiveMode right-click paste", () => {
|
||||
it("feeds clipboard text to the focused component as a bracketed paste", async () => {
|
||||
clipboardMocks.readClipboardText.mockResolvedValue("clipboard text");
|
||||
const handleInput = vi.fn<(data: string) => void>();
|
||||
const target = { render: () => [], invalidate: () => {}, handleInput } satisfies Component;
|
||||
const requestRender = vi.fn();
|
||||
const context = {
|
||||
renderer: { getFocusedComponent: () => target },
|
||||
ui: { requestRender },
|
||||
};
|
||||
const prototype = InteractiveMode.prototype as unknown as {
|
||||
handleRightClickPaste(this: typeof context): Promise<void>;
|
||||
};
|
||||
|
||||
await prototype.handleRightClickPaste.call(context);
|
||||
|
||||
expect(handleInput).toHaveBeenCalledWith("\x1b[200~clipboard text\x1b[201~");
|
||||
expect(requestRender).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
type CopyCommandContext = {
|
||||
session: { getLastAssistantText: () => string | undefined };
|
||||
ui: ReturnType<typeof createInteractiveTui>;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
- Added unbound half-page transcript scrolling actions, `tui.altScreen.halfPageUp` and `tui.altScreen.halfPageDown`, for fullscreen TUI keybindings ([#7735](https://github.com/earendil-works/pi/issues/7735)).
|
||||
- Added double-click word selection, word-aware drag selection, and triple-click line selection in the fullscreen TUI ([#7725](https://github.com/earendil-works/pi/issues/7725)).
|
||||
- Added an optional right-click paste handler to the alternate-screen TUI, currently enabled on Windows.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ export interface TuiAltScreenOptions {
|
|||
mouse?: boolean;
|
||||
/** Open an OSC 8 hyperlink activated with a primary-button click. */
|
||||
openUrl?: (url: string) => void;
|
||||
/** Handle an unmodified secondary-button press for clipboard paste. Currently enabled on Windows only. */
|
||||
onRightClickPaste?: () => void;
|
||||
}
|
||||
|
||||
/** Alternate-screen TUI with a scrollable, application-owned viewport. */
|
||||
|
|
@ -156,6 +158,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|||
private readonly wheelScrollLines: number;
|
||||
private readonly mouseEnabled: boolean;
|
||||
private readonly openUrl?: (url: string) => void;
|
||||
private readonly onRightClickPaste?: () => void;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
|
|
@ -175,6 +178,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|||
this.wheelScrollLines = Math.max(1, Math.floor(options.wheelScrollLines ?? 1));
|
||||
this.mouseEnabled = options.mouse ?? true;
|
||||
this.openUrl = options.openUrl;
|
||||
this.onRightClickPaste = options.onRightClickPaste;
|
||||
this.addInputListener((data) => this.handleViewportInput(data));
|
||||
}
|
||||
|
||||
|
|
@ -406,6 +410,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|||
}
|
||||
const mouseEvent = this.parseSgrMouseEvent(data);
|
||||
if (mouseEvent) {
|
||||
if (this.handleRightClickPaste(mouseEvent)) return { consume: true };
|
||||
const handled = this.handleScrollbarMouseEvent(mouseEvent);
|
||||
if (!this.scrollbarDrag) this.updateScrollbarHover(mouseEvent.x, mouseEvent.y);
|
||||
if (!handled) this.handleSelectionMouseEvent(mouseEvent);
|
||||
|
|
@ -506,6 +511,18 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
|
|||
};
|
||||
}
|
||||
|
||||
private handleRightClickPaste(event: SgrMouseEvent): boolean {
|
||||
if (!this.onRightClickPaste || process.platform !== "win32" || event.release || event.button !== 2) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
this.onRightClickPaste();
|
||||
} catch {
|
||||
// Clipboard paste is best-effort.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private getScrollbarTargetAt(x: number, y: number): ScrollbarTarget | undefined {
|
||||
if (this.hasOverlay() || !this.currentLayout) return undefined;
|
||||
for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
|
||||
|
|
|
|||
|
|
@ -209,6 +209,32 @@ describe("TuiAltScreen", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("invokes the right-click paste handler only on Windows", () => {
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
assert.ok(platformDescriptor);
|
||||
const terminal = new VirtualTerminal();
|
||||
let pasteCount = 0;
|
||||
const tui = new TuiAltScreen(terminal, undefined, undefined, {
|
||||
onRightClickPaste: () => {
|
||||
pasteCount += 1;
|
||||
},
|
||||
});
|
||||
try {
|
||||
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
|
||||
tui.start();
|
||||
terminal.sendInput("\x1b[<2;1;1M");
|
||||
terminal.sendInput("\x1b[<2;1;1m");
|
||||
assert.strictEqual(pasteCount, 1);
|
||||
|
||||
Object.defineProperty(process, "platform", { configurable: true, value: "linux" });
|
||||
terminal.sendInput("\x1b[<2;1;1M");
|
||||
assert.strictEqual(pasteCount, 1);
|
||||
} finally {
|
||||
tui.stop();
|
||||
Object.defineProperty(process, "platform", platformDescriptor);
|
||||
}
|
||||
});
|
||||
|
||||
it("drags a visible scrollbar thumb and keeps it visible until release", async () => {
|
||||
const terminal = new RecordingTerminal(10, 5);
|
||||
const tui = new TuiAltScreen(terminal);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue