Add find-in-scrollback, unicode11, and keyboard pane navigation

Three xterm.js features, implemented together because they share the
XtermPane mount + the single attachCustomKeyEventHandler:

- Unicode 11: load @xterm/addon-unicode11, set activeVersion='11' after
  the canvas renderer so emoji/CJK/box-drawing widths stop drifting.
- Find in scrollback: @xterm/addon-search + a new per-pane SearchBar
  overlay (Ctrl+Shift+F to open, Enter/Shift+Enter next/prev, regex +
  case toggles, Esc to close & refocus). Overlay is an absolutely-
  positioned sibling in a position:relative wrapper so fit() is unaffected.
- Pane navigation: Ctrl+Alt+Arrow / Ctrl+Alt+HJKL (spatial neighbour via
  findNeighborInDirection) and Alt+1..9 (Nth leaf in walkLeaves order).
  XtermPane emits a NavigateIntent; App resolves the target leaf and sets
  it active, reusing the existing isActive->focusTrigger refocus chain.

All chords live in one attachCustomKeyEventHandler (xterm replaces the
handler on each call). Shortcuts added to shortcuts.ts (SoT for README +
Help), including the Alt+digit shell-conflict caveat. tsc clean apart
from the three not-yet-installed addon modules.

Needs pnpm install on the Windows host + runtime verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-28 21:51:29 +01:00
parent 8bb080345e
commit baa00dfc5c
8 changed files with 526 additions and 29 deletions

View file

@ -1,5 +1,5 @@
import { createContext, useContext, type ReactNode } from "react";
import type { Orientation, NodeId, LeafShellSpec } from "./tree";
import type { Orientation, NodeId, LeafShellSpec, Direction } from "./tree";
import type { PaneId, SshHost } from "../../ipc";
/**
@ -62,6 +62,16 @@ export interface Orchestration {
* The PTY stays alive across the move (the new window's XtermPane
* adopts the existing PaneId; scrollback ring is replayed). */
moveToNewWindow: (leafId: NodeId) => void;
/**
* Navigate focus from within a pane's key-handler. XtermPane emits the
* intent; LeafPane/App resolve the target leaf and set it active.
*
* `{ kind: "direction", dir }` move to the spatial neighbour in that
* direction using the same flattenLayout geometry as Ctrl+Shift+Arrow.
* `{ kind: "index", n }` focus the Nth leaf in DFS (walkLeaves) order,
* 1-indexed, clamped to the leaf count (so Alt+9 with 3 panes picks pane 3).
*/
navigateTo: (intent: NavigateIntent) => void;
/** Returns a PaneId only for leaves that just arrived via a window
* transfer (so LeafPane can pass `existingPaneId` to XtermPane to skip
* the spawn). One-shot App clears the entry once the pane has
@ -69,6 +79,13 @@ export interface Orchestration {
getInitialPaneIdFor: (leafId: NodeId) => PaneId | undefined;
}
/** Discriminated intent emitted by XtermPane's key handler. App resolves
* the actual target leaf from the current tree without XtermPane needing
* to know anything about layout geometry or leaf ordering. */
export type NavigateIntent =
| { kind: "direction"; dir: Direction }
| { kind: "index"; n: number };
const OrchestrationContext = createContext<Orchestration | null>(null);
export function OrchestrationProvider({