fix(desktop): wheel-scroll opencode transcripts and surface native Windows notifications (#2422)
This commit is contained in:
parent
0f2295c0c0
commit
71fc914afa
|
|
@ -70,6 +70,14 @@ process.stderr.on("error", ignoreStdStreamError);
|
|||
// Must run before app ready so the About panel and default-menu role labels use it.
|
||||
app.setName("Agent Orchestrator");
|
||||
|
||||
// Windows shows native toasts only when the app declares an AppUserModelID that
|
||||
// matches its installer shortcut (the NSIS maker's appId). Without it,
|
||||
// Notification.isSupported() still returns true but show() silently drops the
|
||||
// toast, so notifications never appear. No-op on macOS/Linux.
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId("dev.agent-orchestrator.desktop");
|
||||
}
|
||||
|
||||
// Pin ALL Electron-owned state (Chromium cache, cookies, local/session storage,
|
||||
// crash dumps) under the canonical AO home at ~/.ao instead of Electron's macOS
|
||||
// default ~/Library/Application Support/<name>. Keeps the app's entire footprint
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ export function TerminalPane({ session, theme, daemonReady, terminalTarget, font
|
|||
);
|
||||
}
|
||||
|
||||
// Agents whose full-screen TUI keeps its own transcript and scrolls it only by
|
||||
// keyboard, ignoring SGR wheel reports. The terminal routes the wheel to
|
||||
// PageUp/PageDown for these (see XtermTerminal's paneScrollsByKeyboard).
|
||||
const KEYBOARD_SCROLL_PROVIDERS = new Set(["opencode"]);
|
||||
|
||||
function bannerText(state: TerminalSessionState, error?: string): string | undefined {
|
||||
if (state === "reattaching") return "Terminal disconnected — reattaching…";
|
||||
if (state === "error") return `Terminal error: ${error ?? "connection failed"}`;
|
||||
|
|
@ -74,6 +79,7 @@ function AttachedTerminal({ session, theme, daemonReady, terminalTarget, fontSiz
|
|||
const queryClient = useQueryClient();
|
||||
const { attach, state, error } = useTerminalSession(attachSession, { daemonReady });
|
||||
const handleId = attachSession?.terminalHandleId;
|
||||
const provider = terminalTarget?.kind === "reviewer" ? terminalTarget.harness : session?.provider;
|
||||
const hadAttachmentRef = useRef(false);
|
||||
const canRestoreSession = terminalTarget?.kind !== "reviewer" && session?.status === "terminated";
|
||||
|
||||
|
|
@ -154,6 +160,7 @@ function AttachedTerminal({ session, theme, daemonReady, terminalTarget, fontSiz
|
|||
fontSize={fontSize}
|
||||
onError={handleInitError}
|
||||
onReady={handleReady}
|
||||
paneScrollsByKeyboard={provider ? KEYBOARD_SCROLL_PROVIDERS.has(provider) : false}
|
||||
theme={theme}
|
||||
/>
|
||||
{showEmptyState && (
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const state = vi.hoisted(() => ({
|
|||
wheelHandler?: (event: WheelEvent) => boolean;
|
||||
selection: string;
|
||||
options: Record<string, unknown>;
|
||||
modes: { bracketedPasteMode: boolean };
|
||||
modes: { bracketedPasteMode: boolean; mouseTrackingMode: string };
|
||||
dataListeners: Set<(data: string) => void>;
|
||||
keyListeners: Set<(event: { key: string }) => void>;
|
||||
selectionListeners: Set<() => void>;
|
||||
|
|
@ -31,7 +31,7 @@ vi.mock("@xterm/xterm", () => ({
|
|||
selection = "";
|
||||
keyHandler?: (event: KeyboardEvent) => boolean;
|
||||
wheelHandler?: (event: WheelEvent) => boolean;
|
||||
modes = { bracketedPasteMode: false };
|
||||
modes = { bracketedPasteMode: false, mouseTrackingMode: "vt200" };
|
||||
dataListeners = new Set<(data: string) => void>();
|
||||
keyListeners = new Set<(event: { key: string }) => void>();
|
||||
selectionListeners = new Set<() => void>();
|
||||
|
|
@ -492,6 +492,46 @@ describe("XtermTerminal", () => {
|
|||
expect(onInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends PageUp/PageDown instead of SGR reports when the pane app has mouse tracking off", () => {
|
||||
const onInput = vi.fn();
|
||||
render(<XtermTerminal theme="dark" onReady={(terminal) => terminal.onUserInput(onInput)} />);
|
||||
state.lastTerminal!.modes.mouseTrackingMode = "none";
|
||||
|
||||
// A keyboard-scroll TUI: one page key per notch regardless of line count,
|
||||
// so 3 lines up => a single PageUp.
|
||||
expect(state.lastTerminal!.wheelHandler!({ deltaY: -50 } as WheelEvent)).toBe(false);
|
||||
expect(onInput).toHaveBeenLastCalledWith("\x1b[5~", "wheel");
|
||||
|
||||
expect(state.lastTerminal!.wheelHandler!({ deltaY: 20 } as WheelEvent)).toBe(false);
|
||||
expect(onInput).toHaveBeenLastCalledWith("\x1b[6~", "wheel");
|
||||
});
|
||||
|
||||
it("sends PageUp/PageDown on Windows even when the pane app tracks the mouse (conpty, no mux)", () => {
|
||||
setNavigatorPlatform("Win32");
|
||||
const onInput = vi.fn();
|
||||
render(<XtermTerminal theme="dark" onReady={(terminal) => terminal.onUserInput(onInput)} />);
|
||||
// opencode enables full mouse tracking but scrolls its transcript only by
|
||||
// keyboard; with no mux to consume SGR reports, Windows must use page keys.
|
||||
state.lastTerminal!.modes.mouseTrackingMode = "any";
|
||||
|
||||
expect(state.lastTerminal!.wheelHandler!({ deltaY: -50 } as WheelEvent)).toBe(false);
|
||||
expect(onInput).toHaveBeenLastCalledWith("\x1b[5~", "wheel");
|
||||
|
||||
expect(state.lastTerminal!.wheelHandler!({ deltaY: 20 } as WheelEvent)).toBe(false);
|
||||
expect(onInput).toHaveBeenLastCalledWith("\x1b[6~", "wheel");
|
||||
});
|
||||
|
||||
it("sends PageUp/PageDown for keyboard-scroll panes even under a mux (opencode on macOS/Linux)", () => {
|
||||
const onInput = vi.fn();
|
||||
render(<XtermTerminal theme="dark" paneScrollsByKeyboard onReady={(terminal) => terminal.onUserInput(onInput)} />);
|
||||
// Linux (beforeEach) + mouse tracking on: without the paneScrollsByKeyboard
|
||||
// hint this would send SGR reports; the hint forces page keys.
|
||||
state.lastTerminal!.modes.mouseTrackingMode = "any";
|
||||
|
||||
expect(state.lastTerminal!.wheelHandler!({ deltaY: -50 } as WheelEvent)).toBe(false);
|
||||
expect(onInput).toHaveBeenLastCalledWith("\x1b[5~", "wheel");
|
||||
});
|
||||
|
||||
it("opens terminal links via window.open so Electron routes them to the OS browser", () => {
|
||||
const open = vi.spyOn(window, "open").mockReturnValue(null);
|
||||
render(<XtermTerminal theme="dark" />);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@ export type XtermTerminalProps = {
|
|||
className?: string;
|
||||
fontSize?: number;
|
||||
theme: Theme;
|
||||
/**
|
||||
* The pane app scrolls its transcript by keyboard (PageUp/PageDown) rather
|
||||
* than acting on SGR wheel reports — e.g. opencode, which enables mouse
|
||||
* tracking but never scrolls on wheel reports. Routes the wheel to page keys
|
||||
* on every platform (see the wheel handler), fixing it under a mux too.
|
||||
*/
|
||||
paneScrollsByKeyboard?: boolean;
|
||||
/** Terminal construction failed; the owner decides how to surface it. */
|
||||
onError?: (error: unknown) => void;
|
||||
/**
|
||||
|
|
@ -176,6 +183,16 @@ function sgrWheelReport(button: number, count: number): string {
|
|||
return `\x1b[<${button};1;1M`.repeat(count);
|
||||
}
|
||||
|
||||
// PageUp (CSI 5~) / PageDown (CSI 6~) for pane apps that scroll their transcript
|
||||
// by keyboard rather than mouse reports. One page key per wheel notch: a page
|
||||
// already scrolls a full screen, so scaling by line count would over-scroll.
|
||||
const PAGE_UP = "\x1b[5~";
|
||||
const PAGE_DOWN = "\x1b[6~";
|
||||
|
||||
function pageKeyReport(lines: number): string {
|
||||
return lines < 0 ? PAGE_UP : PAGE_DOWN;
|
||||
}
|
||||
|
||||
function forceSelectionMode(term: Terminal): void {
|
||||
const internal = term as XtermInternal;
|
||||
const selectionService = internal._core?._selectionService;
|
||||
|
|
@ -486,6 +503,21 @@ export function XtermTerminal(props: XtermTerminalProps) {
|
|||
wheelAccumPx -= lines * rowHeight;
|
||||
}
|
||||
if (lines === 0) return false;
|
||||
// The SGR wheel path exists to drive tmux/zellij copy-mode on
|
||||
// macOS/Linux. It cannot scroll a full-screen TUI that keeps its own
|
||||
// transcript and only scrolls on PageUp/PageDown (opencode): the report
|
||||
// is either consumed by the mux or handed to an app that ignores it.
|
||||
// Send page keys for such apps (paneScrollsByKeyboard), on Windows
|
||||
// (conpty has no mux, so SGR reaches the app and is ignored), and for
|
||||
// any pane app with mouse tracking fully off.
|
||||
if (
|
||||
callbacksRef.current.paneScrollsByKeyboard ||
|
||||
isWindowsPlatform() ||
|
||||
term.modes.mouseTrackingMode === "none"
|
||||
) {
|
||||
emitUserInput(pageKeyReport(lines), "wheel");
|
||||
return false;
|
||||
}
|
||||
const button = lines < 0 ? SGR_WHEEL_UP : SGR_WHEEL_DOWN;
|
||||
emitUserInput(sgrWheelReport(button, Math.abs(lines)), "wheel");
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue