input-and-windows

Implementation rules for terminal keyboard input, focus, windows, tabs, dictation, selection, and secret fields in a visionOS app. Focus is managed centrally because each visionOS window acts as its own keyboard scene.

In plain words
What is it for?
Use it when changing keyboard focus, terminal input, window routing, tab movement, keyboard avoidance, dictation, selection modes, or secure text fields.
Why use it?
It prevents typing from becoming stuck in the wrong terminal and handles floating keyboards and moving windows correctly. It also defines how hidden or compact interface areas should treat focus.

Agent

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add agents/multiplex-term/multiplex/input-and-windows
Clone the repo
git clone --depth 1 https://github.com/multiplex-term/Multiplex
Per session 0 Only the description is in the session, so the agent can decide to use it. The body loads when it is invoked.
When invoked 10,243 The whole file, excluding the scripts and references it only reads on demand.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.00000 $0.10243
Opus 5 $0.00000 $0.05122
Sonnet 5 $0.00000 $0.02049
Haiku 4.5 $0.00000 $0.01024

Measured 2d ago against content hash 63ded570a062, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

input-and-windows scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

docs/agents/input-and-windows.md · 602 lines

How it starts

The opening of the file, as written. The whole thing — 602 lines — stays where its author put it; the contents beside it link to each section on GitHub.

Terminal input, windows & scenes

Load-bearing decisions split from AGENTS.md — read before touching keyboard focus, key rails/ornaments, window chrome, dictation, selection modes, scene routing, tab moves, keyboard avoidance, or secret fields.

  • Keyboard focus goes through TerminalFocusArbiter, never per-view: every visionOS window is its own always-key scene, so per-view responders leave input stuck on the first session; claiming resigns the previous owner and activates the claimed scene. Its visibility tracking must ride the frame-change notifications (KeyboardAvoidance.isPresented, pure): floating keyboards never post didShow/didHide, and stuck-false visibility makes a terminal tap tear down the input session. Stage Manager's zero-height frame while a floating keyboard stays active is ambiguous, not a hide — preserve the previous state. Stage Manager also transiently clears a moving window's isKeyWindow: an already-owned claim must stay a no-op, never makeKey(). The compact shell's deck is a focus-exclusion surface: restoration passes stage visibility through TerminalFocusArbiter.restore and refuses/resigns a hidden responder.
  • The shell spends the safe areas; its panes hand back the clearance (SingleWindowShell). The GeometryReader stays inside the safe area (ignoring an edge zeroes that edge's inset), the pane stack spans the bands, and each pane restores its own: FleetWall as wall padding (shellSafeArea), the terminal as content insets (contentSafeArea, down to KeyBarRow's own padding inside the ViewThatFits proposal). Traps: a landscape Dynamic Island sits mid-edge and iOS reports BOTH landscape edges unsafe without saying which holds it — no band is safe to read in; heights are height + safeArea.bottom (this reader is inset by whichever bottom region applies, so adding it back lands on the window edge, keyboard or not). The key rail takes the bottom strip at compact vertical size class and on every iPad stage (shell or classic window) — the rail IS the window's bottom edge there, so a reserved strip would only park a dead band under the row. Where it does, SwiftTermView.railOwnsBottomSafeArea moves restingBottom with it — a static fact per pane, never read from the live frame (the strip's padding would feed back) — and the rail buys its own daylight back below the key faces (TerminalKeyBar.keyBottomInset(isIOSAppOnMac:spendsBottomStrip:): 3 → 8, which is also the iPhone-landscape padding fix, so the bar's height moves with the fact). The auxiliary panes' rails instead paint through the strip and lift their controls by the classic window's contentSafeArea .bottom, which is why a classic window now hands that inset down. Panes are placed with .offset (claims no width): align the frame .topLeading or SwiftUI centers the lot.
  • iPhone's left-edge right-swipe arbitrates above SwiftTerm (ShellBackSwipeRecognizer, one UIScreenEdgePanGestureRecognizer on the shell window): at the edge, horizontal movement gets first refusal; vertical intent fails immediately back to SwiftTerm (remote scrolling preserved); an active local selection rejects the touch before tracking and re-checks at begin. A SwiftUI gesture does not reliably receive drags begun inside TerminalView.
  • The iPad/iPhone key rail is app-owned chrome, never an inputAccessoryView (TerminalKeyBar): ESC / latching CTRL / TAB, the shell symbols ~ | / -, DECCKM-aware autorepeat arrows, RET (iPad, and iPhone while locked), autorepeating PgUp/PgDn (CSI 5~/6~), and one slot that is the keyboard toggle or — with a physical keyboard — the dictation key. Keep TerminalView.inputAccessoryView = nil: a physical-iPad A/B proved TextInputUI rehosts even a custom accessory during Stage Manager moves, stalling the UI. The keyboard key holds (~0.5 s) into the keyboard lock (TerminalFocusArbiter.lock — a zero-size custom inputView, so the input session, rail, and hardware keys stay live while taps stop summoning); a KEYBOARD LOCKED tip top-center carries the dictation action, latching while permissions resolve then yielding to the LISTENING bar (it shares the dictation bar's slot — the top-trailing slot belongs to window chrome the tip used to cover). Without a physical keyboard, the lock is also a named action in the menu at every width (toggleKeyboardLock; wide chrome passes displacesDirectActions: false so it never duplicates chips). HardwareKeyboardMonitor hides the redundant LOCK action while a keyboard is connected (an already-held lock keeps UNLOCK reachable). State is app-wide (KeyboardLock.shared, arbiter-written, never persisted); claim re-applies it to whichever terminal takes focus. Narrow tiers drop page keys, then symbols; SingleWindowShellLayout's 390/420 pt cutoffs must stay in lockstep with the measured KeyBarRow tiers (375 pt locked floor keeps RET; narrow locked phones move TMUX to a top-right button, and the overflow deliberately has no duplicate tmux entry). "TMUX" names the shortcut-key SLOT: a herdr tab fills it with HRDR — four mono characters, so every tier and cutoff holds unchanged — and both open the shared ShortcutPanelViewController, whose content (ShortcutPanelContent.tmux/.herdr) is the ONE place a backend's shortcut set lives. Pane cycling (both backends) and herdr tab cycling are deliberately absent because those targets are already directly tappable; tmux Last Window is absent because the live switch list targets a window directly. The panel root scrolls when a phone popover clamps its height — never clip rows. The top popover opens downward; while presented, the arbiter resigns the terminal (a docked keyboard would clip the grid) and restores only if that tab still owns focus. Every key sends through TerminalView.send → delegate → ordered pump — never a side channel; CTRL rides controlModifier, latch released on .terminalViewControlModifierReset. Hold CTRL (0.3 s, KeyCommandPanelViewController.controlHoldDuration; a tap still latches and never fires the hold) opens KEY COMMANDS — the recognized hold cancels the control's tap at source, and opening it first retires any already-raised C / B quick slab, so the two CTRL surfaces never coexist. The panel is anchored to the CTRL key on both platforms (KeyCommandPanelViewController in KeyCommandPanel.swift; ONE KeyCommandPanelPresenter owns present / dismiss / focus-resume / popover delegate for both hosts — TerminalKeyBar and the visionOS TerminalKeyClusterGroupView supply only the anchor and their appearance step, the ornament carrying the C / B popover's appearance and glass mirroring). The panel stands on the shortcut panel's now-shared grammar (ShortcutPanelRootView, ShortcutPressControl, TallyHairlineGrid, TallyPanelHeader, UIKitChassisMonoLabel) and the agent editor's shared controls in Design/TallyEditorControls.swift (TallyEditorSwitch, TallyEditorRowActionButton, TallyEditorRowActions, TallyEditorLegend, TallyEditorFooter). Two tabs: COMMANDS is a two-column hairline grid of saved rows (tap sends; a row with CLOSE ON PRESS off keeps the panel up; a 0.5 s hold jumps to that row's setup); CUSTOM SETUP is the agent editor's numbered list with an inline composer (TYPE KEYS | TEXT, ⌃ ⇧ ⌥ + one key from ↩ ⇥ ⎋ ⌫ ␣ ↑ ↓ ← → or a one-character field, or one line of text with SUBMIT = CR ~160 ms later, REPEAT count + gap under the guard ×5 · 50–500 ms · burst ≤ 2 s, PANEL, SENDS readout of the live bytes) — a draft transaction, DONE normalizes once, CANCEL/outside dismissal discards. The model (KeyCommand, KeyChord, KeyTextSnippet, KeyCommandSet with the three shipped defaults ⇧↩ · ⌃C×2 · ⌥⌫-stays, KeyCommandRepeatGuard) is pure; chords are never bytesKeyCommandDispatcher asks the fork (TerminalView.bytes(for:), fourteenth patch group) at press time, so a chord sends exactly what a hardware press would in the terminal's current mode (kitty flags, DECCKM, backspace mode), and text rows ride send(txt:) + a delayed CR like slash chips. Every send is TerminalView.send, i.e. the ordered pump. Storage is app-wide (KeyCommandStore.shared: keycommands.json beside hosts.json plus one synchronizable Keychain item; KeyCommandSync.merge is the pure last-writer-wins rule; refreshed at launch and when the panel opens, at most once a minute — no CloudKit/KVS entitlement exists). Tier: free keeps EntitlementStore.freeKeyCommandLimit (5) commands, Pro the model's cap (12) — the same rule as hosts: a set that already holds more (synced from a Pro device) is never trimmed and every row keeps sending; only ADD COMMAND is gated, turning into the prominent "ADD COMMAND · PRO" chip that opens the paywall (a legend line says why the cap is 5). The rail and cluster never learn about Pro: the terminal window builds a KeyCommandPlan (limit + paywall route) from its EntitlementStore and hands it down — iPad through TerminalPaneConfigurationTerminalSurfaceView.ConfigurationTerminalKeyBar.keyCommandPlan, visionOS onto both TerminalKeyClusterContexts (shell + ornament) — and the presenter wraps the route so the popover is down before the paywall sheet presents (presentPaywall refuses while anything is presented). entitlements.isPro is in the window's observation set, so a purchase re-renders with the lifted plan. The terminal GUIDE carries a HOLD CTRL card (figure 12) and a MESSAGE BOX card (figure 13). Focus: the panel never suspends the terminal; only if one of its own fields took the keyboard does dismissal resumeAfterPresentation. Design record + grill: local-plan/key-commands-bakeoff/. visionOS: TerminalKeyCluster (same keys + latch + DEBUG hook) flanks the UMD on ONE console line in the bottom ornament. ViewThatFits compacts key faces first; when even compact can't fit, a fixedSize floor lets the row overflow the window edges symmetrically. Never re-add a keys-under-UMD restack (the system CLIPS ornament content hanging below the anchor) and never let the UMD compress (its title truncates). UIHostingOrnament centers the root's geometric bounds; a descendant alignment guide does not cross that host. TerminalVisionConsoleLayout therefore makes the console row's top the root's actual midpoint, with the helper in the upper half. Do not regress it to a plain VStack/alignment guide: helper-less keys will straddle the window until an agent grows the stack. Re-verify visually when touching it. DECK and the text-size chips live in the UMD title row. ⚠ SwiftTerm still builds its stock accessory on visionOS and commitTextInput prefers its controlModifierSwiftTermView nils inputAccessoryView there; don't remove that.
  • Arrange Keys (2026-08-27) → Arrange Keys… (a menu row: every key hold is taken and a held key is unfindable); per tab, never persisted (TerminalSessionController.keyBarArranging). The ORDER is a slot permutation (KeyBarOrder; a slot names a position — keyboard/mic share .keyboard, TMUX/HRDR .shortcuts), device-local in KeyBarOrderStore; gaps and widths never move (rail groups are counted at layout, the ornament's ten keys split 3 lead / 7 trail, CTRL's slab follows the key). The move is the tab strip's drag and drop: each key is a UIDragInteraction source (TerminalChromeDragItem, the item recipe tabs share — the pane's file gate refuses any TerminalChromeDragPayload), one KeyBarDropCoordinator is the drop delegate for the rail and the cluster (KeyBarDropSurface); target = nearest other key by centre (RowDropGeometry), rightward lands after it, leftward before (KeyBarOrder.moving moves only the dragged key in the full order). Never a custom recognizer drag: the keys stay live and hoverable (visionOS gaze targets interactive elements) and the system preview is the one thing that floats over the UMD's glass. Rails reconcile the order in layout, never in the render signature, so nothing re-creates a key mid-mode. iPad: the ARRANGE KEYS bar (RESET · DONE) hangs over the rail like the C / B slab (mountOverRail); leaving the window ends the mode. visionOS: drop hosts are both slabs plus the UMD and bar slabs (a release over the title lands beside the nearer key); the bar is ornament role .arrange below the console row, one ArrangeKeysBarViewController per tab that watches the store for RESET; a tab switch ends the mode; GUIDE rides the (guideIsDirectChip). Proofs: debug.arrangekeys, debug.arrangekeysmove, build.sh uitest ipad (the visionOS sim lifts nothing; fake UIDropSession tests cover the coordinator).
  • Auxiliary tabs wear their whole bottom chrome in the ornament (2026-08-10) — on classic visionOS windows the in-window ▤/⌗ rails are not mounted (showsInWindowRail: false); their end chips collided with the system resize corners. The ▤ file viewer stacks its file row above the verbatim UMD row in ONE slab (ViewportUMDRootView .installStackedDeck; the rail's duplicate CLOSE is deleted — the window row's CLOSE is the only one). The ⌗ viewport replaces rail + UMD with three content-sized slabs (ViewportSwitchboardViewController: navigate DECK ◂ ⟳ / locate address+reach / act SYSTEM ⋯ CLOSE; the caps ⌗ port · host title is retired — the address readout is the identity, and MERGE folds into ⋯). Both mount .fixedSize() — NEVER window-width: a slab that tracks the window permanently covers the system resize bar (shipped and caught same day). TerminalVisionStackedDeckGeometry biases the deck so its top edge sits half a window row above the anchor and the extra file row hangs BELOW it, off the pane's last lines; auxiliary slab content below the anchor does render — the CLIPS trap above is about restacking keys under the UMD console line. Shell/iPad keep the in-window rails unchanged; re-verify placement visually when touching any of this.
  • The side panel never participates in terminal layout or focus. iPad mounts the card over the pane container's inset rectangle, reuses the pane's keyboardObstruction, and the address field borrows the host terminal through TerminalFocusArbiter.lend. Its action row is a header on iPad and a bottom bar on visionOS (SidePanelRowEdge) and surfaces chips in measured priority order — only for what did not fit. Classic visionOS hangs it from a third trailing ornament whose content is ONE static mount: a transparent strip twice the window's width, centred on the glass edge (SidePanelWidth.visionStripWidth(windowWidth:)); the card lives inside it as (width, overhang = right edge past the GLASS) with a handle on each edge above the row, and the strip's empty part passes hits through (SidePanelStripView). Decisions that cost device rounds — don't re-open without new facts: geometry persists GLASS-relative, never strip-relative (a strip-relative inset outlived a strip-width change and pinned the card); a drag FREEZES the card into one blurred bitmap until RELEASE (no mid-drag settle thaw — gaze-and-pinch events arrive with gaps), moves only frames, and under GLASS the system platter steps out for the drag; nothing about the ornament changes per tick (a SwiftUI-driven drag and a per-tick UIKit relayout were both unusable on device) and its content carries no animation/transition. ⚠ visionOS 27 sim only: after ↗ TAB on a ▤ panel the glass keeps the old frame 3–6 s (CPU idle; ⌗, iPad and the shell present in ≤ 1 s) — judge on device, don't chase it.
  • A terminal window's title bar is app-owned, and its scene asks for .minimal window controls (TerminalClassicRailInsets; MultiplexSceneDelegate.preferredWindowingControlStyle(for:)). Two system pieces had to go. The navigation bar: iPadOS 26+ sizes it for system controls and it CANNOT be shortened — measured 54 pt above a 10 pt scene inset (64 pt of chrome for 21 pt faces), where sizeThatFits is consulted and ignored, a frame clamp never lands (constraint-driven layout), and a bounds clamp shrinks the bar but leaves UIKit centring it in the band it still reserves, pushing the content inset to 74.5 pt. Don't retry those; the classic window mounts the same UMDBarViewController .shell rail a full-screen iPad wears, and the navigation controller stays only as the hosting plan with its bar hidden. The window-control pill: iPadOS 26's default .unifiedStyle parks it INSIDE the scene's content, 21–43 pt below the window's top edge, and nothing app-side moves it (hidden status bar, no navigation controller, rail heights 54/41/31 all left it exactly there). .minimalStyle — "occupy as little of the scene's space as possible" — lifts it to 6–27.5 pt, in line with the rail's own chips. It is scoped to terminal scenes: the deck and shell still host system navigation bars, and .unified is what insets THOSE bars around the pill. Total chrome: 44 pt in a window — the rail matches TerminalKeyBar.barHeight at the pane's other end (minimumContentHeight; the faces keep their size and centre in it, and the padding becomes a floor) — pill contained, versus the old 64 pt. The rail spends a top strip ONLY where the scene reaches the display's top chrome, and never on the Mac (its scene already sits below a real title bar). Both halves are caught regressions: spending it always paints the row below a floating window's pill and under a second, empty Mac title bar; spending it never paints the rail UNDER the status bar when the window is maximised. ⚠ Under iPadOS 26 windowing NONE of the obvious signals answer this (all measured 2026-08-04): safeAreaInsets.top reports the display's 32 pt status bar even for a 711x941 window floating in the middle of a 1032x1376 display, statusBarManager likewise answers for the display, and UIWindowScene.isFullScreen is Mac Catalyst's property — it stays false even when the window is maximised. ⚠ Nothing reports a window MOVE either: UIWindowSceneGeometry on iOS carries no origin (coordinate space, orientation, isInteractivelyResizing; systemFrame is Mac Catalyst only), so windowScene(_:didUpdateEffectiveGeometry:) — iOS 26's replacement for the deprecated didUpdateCoordinateSpace, adopted here — covers resizes and screen moves but NOT a drag, and the rail kept stale insets (user-reported as "doesn't always expand or collapse when the window is moved"). A 0.5 s position watch closes it: one rect conversion per tick while the scene is active, layout invalidated only on a real change — watching is polling, the deck's way. Geometry is what's left, and a window's own frame is scene-relative (origin always zero) — the position comes from window.convert(window.bounds, to: screen.coordinateSpace), which DOES report where a scene sits on the display (measured: a floating window at y 217.5 on a 1376 pt display). meetsSystemTopChrome (spans the display) compares the dimensions UNORDERED — UIScreen .bounds does not follow the scene's orientation, so a maximised landscape window is 1376x1032 against a 1032x1376 screen. Same trap for anything else asking "am I full screen" — the shell-mode decision's own isFullScreen read is worth re-checking. TMUX/HRDR names one road and exactly one rail may draw it: the pane's key rail owns the chip wherever it fits, and the rail above takes it over only at the widths where the key rail drops it. The rail therefore measures the KEY RAIL's content width (keyRailContentWidth), never its own — a classic window's rail clears the window-control pill and is ~90 pt narrower than the pane below it, while the shell's wide row stays above the cutoff on any landscape phone (drawing a second chip there was a shipped bug, 2026-08-06). No key rail at all (visionOS) means the rail owns the chip outright. MERGE rides the wide row beside DETACH (the overflow carries it only when it displaces the direct actions). ⚠ The clearances are the RAIL's (umdSafeArea), never a pane's (contentSafeArea): the leading inset reaching TerminalPaneConfiguration shoves the terminal and key rail off their own window (shipped-and-caught, 2026-08-04; paneAndRailInsetsForTesting pins them apart). umdAvailableWidth must likewise stay non-nil or the rail never chooses its compact row. There is still NO way to touch the Mac's own title bar from a Designed-for-iPad binary (UITitlebar is Mac Catalyst-only and absent from the iOS SDK).

Read the full file on GitHub · 602 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 2d ago First seen · 602 lines · 0 tokens per session scan A 63ded570a062

Subscribe to this mod's changes

input-and-windows is an agent published in the GitHub repository multiplex-term/Multiplex (10 stars, last pushed 5d ago), licensed Apache-2.0. It costs nothing until one of its globs matches a file; then it loads 10,243 tokens. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.

Related

Other agents, from other repositories

i18n

Hermex ships one String Catalog, HermesMobile/Resources/Localizable.xcstrings, included in the app, widget, and share-extension targets. Every user-facing literal is already externalized (String(localized:) / LocalizedStringKey), so adding a language is normally translation-only — no Swift edits.

uzairansaruzi/hermex · 0 tokens

feature-gap-index

Thin, always-current classification of upstream Hermes-WebUI API route groups against Hermes-Mobile. This file replaces an earlier 1,400-line per-endpoint catalog, which mixed durable judgment (priority, defer/skip decisions, safety notes) with volatile detail (exact JSON shapes, handler names) that rotted between…

uzairansaruzi/hermex · 0 tokens

issue-tracker

Issues and PRDs for this repo live as GitHub issues. Use the gh CLI for issue operations.

uzairansaruzi/hermex · 0 tokens

layer3-issue-detection

Layer 3 systematically scans ALL entry points from Layer 1 and applies issue detection rules. Unlike Layer 2 (which traces specific flows in depth), Layer 3 does a breadth-first scan to categorize issues across the entire codebase.

Terryc21/workflow-audit · 0 tokens

layer5-data-wiring

Layer 5 verifies that features use real user data instead of mock/hardcoded values, and that model capabilities are fully wired into the features that need them. While Layers 1-4 audit navigation and UX, Layer 5 audits whether the data flowing through those workflows is genuine.

Terryc21/workflow-audit · 0 tokens

layer2-methodology

Layer 2 takes entry points from Layer 1 and traces the complete user journey.

Terryc21/workflow-audit · 0 tokens