merge PR #5714 via eval/pr-5714: fix(tui): preserved plan review text selection
This commit is contained in:
@@ -50,6 +50,9 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed unknown `__omp_worker_*` CLI selectors exiting 0 with empty output instead of erroring; an unrecognized worker-host selector now writes `Error: unknown worker selector: …` to stderr and exits nonzero, so a stale or mistyped selector can no longer look healthy to a parent process or install smoke path ([#5712](https://github.com/can1357/oh-my-pi/issues/5712)).
|
||||
### Fixed
|
||||
|
||||
- Fixed Plan Review capturing mouse drags as pointer events, preventing native terminal text selection ([#5711](https://github.com/can1357/oh-my-pi/issues/5711)).
|
||||
|
||||
## [17.0.1] - 2026-07-16
|
||||
|
||||
|
||||
@@ -2599,6 +2599,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
maxHeight: "100%",
|
||||
margin: 0,
|
||||
fullscreen: true,
|
||||
mouseTracking: false,
|
||||
});
|
||||
this.ui.setFocus(overlay);
|
||||
this.ui.requestRender();
|
||||
|
||||
@@ -10,7 +10,7 @@ import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config
|
||||
import { resolveLocalUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
|
||||
import { AssistantMessageComponent } from "@oh-my-pi/pi-coding-agent/modes/components/assistant-message";
|
||||
import type { HookSelectorSlider } from "@oh-my-pi/pi-coding-agent/modes/components/hook-selector";
|
||||
import type { PlanReviewOverlay } from "@oh-my-pi/pi-coding-agent/modes/components/plan-review-overlay";
|
||||
import { PlanReviewOverlay } from "@oh-my-pi/pi-coding-agent/modes/components/plan-review-overlay";
|
||||
import { InteractiveMode } from "@oh-my-pi/pi-coding-agent/modes/interactive-mode";
|
||||
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
@@ -19,7 +19,7 @@ import { SILENT_ABORT_MARKER, USER_INTERRUPT_LABEL } from "@oh-my-pi/pi-coding-a
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import { AUTO_THINKING } from "@oh-my-pi/pi-coding-agent/thinking";
|
||||
import * as clipboard from "@oh-my-pi/pi-coding-agent/utils/clipboard";
|
||||
import { setKeybindings, Text } from "@oh-my-pi/pi-tui";
|
||||
import { type OverlayHandle, type OverlayOptions, setKeybindings, Text } from "@oh-my-pi/pi-tui";
|
||||
import { formatNumber, TempDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
/**
|
||||
@@ -333,6 +333,28 @@ describe("InteractiveMode plan review rendering", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves terminal mouse tracking disabled while Plan Review is open", async () => {
|
||||
let capturedOverlay: PlanReviewOverlay | undefined;
|
||||
let capturedOptions: OverlayOptions | undefined;
|
||||
const overlayHandle: OverlayHandle = {
|
||||
hide: vi.fn(),
|
||||
setHidden: vi.fn(),
|
||||
isHidden: vi.fn(() => false),
|
||||
};
|
||||
vi.spyOn(mode.ui, "showOverlay").mockImplementation((component, options) => {
|
||||
if (!(component instanceof PlanReviewOverlay)) throw new Error("Expected Plan Review overlay");
|
||||
capturedOverlay = component;
|
||||
capturedOptions = options;
|
||||
return overlayHandle;
|
||||
});
|
||||
|
||||
const choice = mode.showPlanReview("# Plan\n\nSelectable body", "Plan mode - next step", ["Approve"]);
|
||||
|
||||
expect(capturedOptions).toMatchObject({ fullscreen: true, mouseTracking: false });
|
||||
capturedOverlay?.handleInput("\x1b");
|
||||
await expect(choice).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("copies the overlay's current edited plan markdown from the real plan review overlay", async () => {
|
||||
let capturedOverlay: PlanReviewOverlay | undefined;
|
||||
const overlayHandle = { hide: vi.fn() };
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
- Fixed Enter accepting a mid-prompt `/skill:<name>` autocomplete from submitting and clearing the draft; acceptance now inserts the skill token and leaves the prompt open ([#4773](https://github.com/can1357/oh-my-pi/issues/4773)).
|
||||
- Fixed Markdown rendering turning local file paths into HTTP links when a `www.` or `http(s)://`/`ftp://` sequence was glued to a preceding character (e.g. `~/meta/www.share/blog/index.dj`); extended autolinks now require a valid GFM left boundary (start of line, whitespace, or one of `*_~(`) ([#5652](https://github.com/can1357/oh-my-pi/issues/5652)).
|
||||
- Fixed multi-row direct Kitty images being clipped or detached from their cells in native terminal scrollback ([#5669](https://github.com/can1357/oh-my-pi/pull/5669) by [@jeffscottward](https://github.com/jeffscottward)).
|
||||
### Added
|
||||
|
||||
- Added a fullscreen overlay mouse-tracking opt-out so selection-first dialogs can preserve native terminal text selection ([#5711](https://github.com/can1357/oh-my-pi/issues/5711)).
|
||||
|
||||
## [17.0.1] - 2026-07-16
|
||||
|
||||
|
||||
+26
-20
@@ -90,11 +90,11 @@ const CURSOR_BEGIN = `${HIDE_CURSOR}${SYNC_OUTPUT_BEGIN}`;
|
||||
const CURSOR_BEGIN_NO_SYNC = HIDE_CURSOR;
|
||||
const CURSOR_END = SYNC_OUTPUT_END;
|
||||
const CURSOR_END_NO_SYNC = "";
|
||||
// Mouse reporting, enabled only for the lifetime of a fullscreen overlay so the
|
||||
// rest of the app keeps the terminal's native text selection. 1000h = button
|
||||
// click tracking, 1003h = any-motion tracking so overlays can light up hover
|
||||
// targets (the pointer moving with no button held), 1006h = SGR extended
|
||||
// coordinates so columns/rows past 223 are reported.
|
||||
// Mouse reporting is scoped to fullscreen overlays that opt into pointer
|
||||
// interaction. 1000h = button click tracking, 1003h = any-motion tracking for
|
||||
// hover targets, and 1006h = SGR extended coordinates past column/row 223.
|
||||
// Selection-first overlays leave these modes disabled so the terminal retains
|
||||
// native text selection.
|
||||
const MOUSE_TRACKING_ON = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
|
||||
const MOUSE_TRACKING_OFF = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
||||
|
||||
@@ -464,6 +464,11 @@ export interface OverlayOptions {
|
||||
* unchanged and still draw over the transcript on the normal screen.
|
||||
*/
|
||||
fullscreen?: boolean;
|
||||
/**
|
||||
* Enable terminal mouse reporting while fullscreen. Defaults on; disable it
|
||||
* when native terminal text selection takes precedence over pointer events.
|
||||
*/
|
||||
mouseTracking?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1096,6 +1101,7 @@ export class TUI extends Container {
|
||||
// untouched, so exiting reconciles cleanly against the terminal-restored
|
||||
// normal screen. #altPreviousLines is the last alt frame, for repaint-skip.
|
||||
#altActive = false;
|
||||
#altMouseTrackingActive = false;
|
||||
#altPreviousLines: string[] = [];
|
||||
#altEnterWidth = 0;
|
||||
#altEnterHeight = 0;
|
||||
@@ -1752,11 +1758,12 @@ export class TUI extends Container {
|
||||
|
||||
stop(): void {
|
||||
if (this.#altActive || this.#pendingAltExit) {
|
||||
const exitSequence =
|
||||
this.#pendingAltExit || `${MOUSE_TRACKING_OFF}${this.#keyboardEnhancementExit()}\x1b[?1049l`;
|
||||
const mouseExit = this.#altMouseTrackingActive ? MOUSE_TRACKING_OFF : "";
|
||||
const exitSequence = this.#pendingAltExit || `${mouseExit}${this.#keyboardEnhancementExit()}\x1b[?1049l`;
|
||||
this.terminal.write(exitSequence);
|
||||
setAltScreenActive(false);
|
||||
this.#altActive = false;
|
||||
this.#altMouseTrackingActive = false;
|
||||
this.#altPreviousLines = [];
|
||||
this.#pendingAltExit = "";
|
||||
}
|
||||
@@ -2776,24 +2783,29 @@ export class TUI extends Container {
|
||||
// requests it, borrow the terminal's alternate buffer and paint only the
|
||||
// modal there; the normal screen and all accounting stay untouched.
|
||||
let deferredAltExit = this.#pendingAltExit;
|
||||
const wantAlt = this.#wantsAltScreen();
|
||||
const topOverlay = this.#getTopmostVisibleOverlay();
|
||||
const wantAlt = topOverlay?.options?.fullscreen === true;
|
||||
const wantMouseTracking = wantAlt && topOverlay.options?.mouseTracking !== false;
|
||||
if (wantAlt && !this.#altActive) {
|
||||
// Enhanced keyboard modes can be buffer-local: re-push the active
|
||||
// modified-key reporting sequence on the freshly entered alternate
|
||||
// screen, or Esc/modified keys revert to legacy encoding inside
|
||||
// fullscreen overlays (Ghostty/kitty/iTerm2).
|
||||
this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementEnter()}${MOUSE_TRACKING_ON}`);
|
||||
const mouseEnter = wantMouseTracking ? MOUSE_TRACKING_ON : "";
|
||||
this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementEnter()}${mouseEnter}`);
|
||||
setAltScreenActive(true);
|
||||
this.terminal.hideCursor();
|
||||
this.#forgetHardwareCursorState();
|
||||
this.#recordHardwareCursorHidden();
|
||||
this.#altActive = true;
|
||||
this.#altMouseTrackingActive = wantMouseTracking;
|
||||
this.#altPreviousLines = [];
|
||||
this.#altEnterWidth = width;
|
||||
this.#altEnterHeight = height;
|
||||
} else if (!wantAlt && this.#altActive) {
|
||||
const mouseExit = this.#altMouseTrackingActive ? MOUSE_TRACKING_OFF : "";
|
||||
const enhancementExit = this.#keyboardEnhancementExit();
|
||||
const exitSequence = `${MOUSE_TRACKING_OFF}${enhancementExit}\x1b[?1049l`;
|
||||
const exitSequence = `${mouseExit}${enhancementExit}\x1b[?1049l`;
|
||||
// Session replacement can finish while a fullscreen selector is still
|
||||
// covering the old normal buffer. Keep the overlay visible until the
|
||||
// replacement is ready, then fuse the buffer restore into that full paint;
|
||||
@@ -2805,6 +2817,7 @@ export class TUI extends Container {
|
||||
setAltScreenActive(false);
|
||||
this.#forgetHardwareCursorState();
|
||||
this.#altActive = false;
|
||||
this.#altMouseTrackingActive = false;
|
||||
this.#altPreviousLines = [];
|
||||
// A resize while on the alt buffer reflowed the terminal's saved
|
||||
// normal screen; it no longer matches our accounting, so force the
|
||||
@@ -2812,6 +2825,9 @@ export class TUI extends Container {
|
||||
if (width !== this.#altEnterWidth || height !== this.#altEnterHeight) {
|
||||
this.#resizeEventPending = true;
|
||||
}
|
||||
} else if (wantMouseTracking !== this.#altMouseTrackingActive) {
|
||||
this.terminal.write(wantMouseTracking ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
||||
this.#altMouseTrackingActive = wantMouseTracking;
|
||||
}
|
||||
if (this.#altActive) {
|
||||
this.#componentRenderTargets.clear();
|
||||
@@ -3776,16 +3792,6 @@ export class TUI extends Container {
|
||||
this.terminal.write(buffer);
|
||||
}
|
||||
|
||||
/** Topmost visible overlay requests the alternate-screen buffer. */
|
||||
#wantsAltScreen(): boolean {
|
||||
for (let i = this.overlayStack.length - 1; i >= 0; i--) {
|
||||
const entry = this.overlayStack[i]!;
|
||||
if (!this.#isOverlayVisible(entry)) continue;
|
||||
return entry.options?.fullscreen === true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose and paint a single fullscreen overlay frame on the alt buffer.
|
||||
* Cursor markers are stripped (the modal draws its own in-band caret and
|
||||
|
||||
@@ -3310,6 +3310,48 @@ describe("TUI terminal-state regressions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves native text selection available for selection-first fullscreen overlays", async () => {
|
||||
const term = new VirtualTerminal(40, 8, 200);
|
||||
const writes = captureWrites(term);
|
||||
const tui = new TUI(term);
|
||||
tui.addChild(new MutableLinesComponent(rows("base-", 8)));
|
||||
|
||||
try {
|
||||
tui.start();
|
||||
await settle(term);
|
||||
|
||||
const showFrom = writes.length;
|
||||
const handle = tui.showOverlay(new MutableLinesComponent(["SELECTABLE PLAN TEXT"]), {
|
||||
anchor: "bottom-center",
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
margin: 0,
|
||||
fullscreen: true,
|
||||
mouseTracking: false,
|
||||
});
|
||||
await settle(term);
|
||||
|
||||
const modalWrites = writes.slice(showFrom).join("");
|
||||
expect(modalWrites).toContain("\x1b[?1049h");
|
||||
expect(modalWrites).not.toContain("\x1b[?1000h");
|
||||
expect(modalWrites).not.toContain("\x1b[?1003h");
|
||||
expect(modalWrites).not.toContain("\x1b[?1006h");
|
||||
expect(visible(term).some(line => line.includes("SELECTABLE PLAN TEXT"))).toBeTrue();
|
||||
|
||||
const hideFrom = writes.length;
|
||||
handle.hide();
|
||||
await settle(term);
|
||||
|
||||
const hideWrites = writes.slice(hideFrom).join("");
|
||||
expect(hideWrites).toContain("\x1b[?1049l");
|
||||
expect(hideWrites).not.toContain("\x1b[?1000l");
|
||||
expect(hideWrites).not.toContain("\x1b[?1003l");
|
||||
expect(hideWrites).not.toContain("\x1b[?1006l");
|
||||
} finally {
|
||||
tui.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to kittyEnableSequence for legacy custom terminals", async () => {
|
||||
const term = new LegacyKeyboardVirtualTerminal(40, 8, 200);
|
||||
const writes = captureWrites(term);
|
||||
|
||||
Reference in New Issue
Block a user