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

@ -98,7 +98,7 @@ import {
presetTwoRows,
presetTwoByTwo,
} from "./lib/layout/tree";
import { OrchestrationProvider, type Orchestration } from "./lib/layout/orchestration";
import { OrchestrationProvider, type Orchestration, type NavigateIntent } from "./lib/layout/orchestration";
import LeafPane from "./lib/layout/LeafPane";
import Gutter from "./lib/layout/Gutter";
import Notifications, { type Toast } from "./components/Notifications";
@ -717,6 +717,36 @@ export default function App() {
setActiveLeafId(leafId);
}, []);
// navigateTo — called from XtermPane's attachCustomKeyEventHandler via
// LeafPane's onNavigate prop. Resolves the target leaf from the current
// layout tree and sets it active; the LeafPane isActive→focusTrigger
// effect then refocuses the xterm textarea automatically.
const navigateTo = useCallback((intent: NavigateIntent) => {
const currentTree = treeRef.current;
const currentActiveId = activeLeafId;
if (intent.kind === "direction") {
const layout = flattenLayout(currentTree);
if (!currentActiveId) {
const first = layout.leaves[0]?.leaf.id;
if (first) setActiveLeafId(first);
return;
}
const nextId = findNeighborInDirection(
layout.leaves,
currentActiveId,
intent.dir,
);
if (nextId) setActiveLeafId(nextId);
} else {
// intent.kind === "index"
const leaves = Array.from(walkLeaves(currentTree));
// Clamp: Alt+9 with 3 panes picks the 3rd pane.
const target = leaves[Math.min(intent.n, leaves.length) - 1];
if (target) setActiveLeafId(target.id);
}
}, [activeLeafId]); // treeRef is a ref — stable, intentionally not listed
const openHostManager = useCallback(() => setHostManagerOpen(true), []);
const closeHostManager = useCallback(() => setHostManagerOpen(false), []);
@ -1242,6 +1272,7 @@ export default function App() {
toggleMcpAllow,
openHostManager,
setActive,
navigateTo,
registerPaneId,
broadcastFrom,
notify,
@ -1266,6 +1297,7 @@ export default function App() {
toggleMcpAllow,
openHostManager,
setActive,
navigateTo,
registerPaneId,
broadcastFrom,
notify,