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.
npx agentmods add agents/multiplex-term/multiplex/links-and-viewersgit clone --depth 1 https://github.com/multiplex-term/MultiplexWhat 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.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5 | $0.00000 | $0.05247 |
| Opus 5 | $0.00000 | $0.02624 |
| Sonnet 5 | $0.00000 | $0.01049 |
| Haiku 4.5 | $0.00000 | $0.00525 |
Grade A, and why
links-and-viewers 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.
How it starts
The opening of the file, as written. The whole thing — 301 lines — stays where its author put it; the contents beside it link to each section on GitHub.
Links, viewport & file viewer
Load-bearing decisions split from AGENTS.md — read before touching terminal link/path resolution, the ⌗ viewport, or the ▤ file viewer.
- A terminal link is confirmed, never followed (
TerminalLink, pure + tested;TerminalLinkSheet). A press is the activation route — a tap over a target the app claims confirms it at any mouse mode, and the remote gets the click only on cells the app declines (2026-08-18; the earlier long-press-only rule kept every tap for the remote under mouse tracking, which made the feature unfindable under tmuxmouse on, this app's default). Accepted trade: over a path or URL, tmux stops switching panes and vim stops placing the cursor — the same trade the visionOS gaze regions already make. Long press stays the second route, local at any mouse mode. Rules: the scheme allowlist handed to the system ishttp/https/mailtoand nothing else — notably NOTmultiplex:, whichExternalActionRouterwould accept, so pane output can't launch an agent on another host. A valid local-authorityfile:URI is the one terminal-only handoff: activation asksTerminalPathTargetFIRST and confirms the decoded absolute path through the file-viewer sheet; malformed/non-local file URIs remain blocked + copy-only here. Filesystem-path matches otherwise resolve to nil here (they confirm through that same path sheet); interior whitespace disqualifies (warning: unused variableis not awarning:link). Schemeless URLs resolve as links too (TerminalLink.schemelessLink): the authority must be domain-shaped (≥2 ASCII labels, alphabetic ≥2-char TLD) or dotted-quad IPv4 (ViewportReach.isIPv4Literal, the one parser), with URL evidence beyond the dot (//?/#rest orwww.— a markdown link'ssetup.mdstays a document), userinfo rejected; the scheme defaults by reach (ViewportReach.classify(host:): http for LAN/loopback, https elsewhere) andrawis the composed URL so the sheet states the chosen scheme. Markdown destinations opt out (resolve(_:schemelessHosts: false)— a schemeless href is a relative reference by spec). A dotted non-allowlisted-scheme candidate declines to nil. The sheet renders the resolved target with the host on its own line (which is what exposeshttps://[email protected]/x); blocked/malformed targets still get a sheet with COPY. The target is an editable field (TerminalSheetEditableValueBox): every keystroke re-runs resolve, and the actions carry the resolved value. ⚠ OSC 8 does not survive a tmux attach today — tmux 3.6a emits hyperlinks only to terminals advertisingHls, and the app requestsTERM=xterm-256color— so implicit detection is the live path in tmux tabs. Don't "fix" it with a server-scopedterminal-featuresdefault (the leak the per-host conf documents). On visionOS links glow under the eye (TerminalLinkHoverOverlay, aTerminalViewsubview): system hover regions stood over the fork's link enumeration, rebuilt debounced on output/scroll — deferred while keystrokes flow (the fork's send-path stamp viahasRecentUserInput; typing echoes retrigger the debounce ~3×/s for no visible benefit), and an unchanged region set is a comparison, never a rebuild (each region is a cross-process hover-effect registration). Hover regions ARE hit regions — a pinch on a lit link outranks the remote click on those cells (accepted trade); only targetsresolvewould confirm get a region; obscured tabs clear theirs. Full record:local-plan/terminal-links.md. - The viewport is summoned, never restored (
TerminalRoute.Mode .viewport;ViewportReach/ViewportOfferpure + tested; bake-off inlocal-plan/viewport-bakeoff/): a confirmed web link docks as a ⌗ tab beside its session; the WKWebView is controller-owned and re-parents through merge/split (live page, HMR socket included, survives); its cell never wears a tally dot. The link sheet stays the only gate and grows a REACH row:localhostprinted in a pane is the host's loopback — the chip becomes⌗ OPEN VIA <host>and rewrites the authority toHost.hostname, said in the open. The rail readout is tap-to-edit: a typed address is the user's own intent, so it skips the sheet but rides the same admit path (ViewportOffer.fromTypedInput; web schemes only, schemeless defaulted by reach, loopback rewritten); the editor lives in the pane's TOP contextual slot (the pane opts out of keyboard avoidance). Controllers live only inTerminalWorkspace's memory and register BEFORE the tab enters any route, sosyncTabsstrips exactly the tabs a dead process restored, never a live move. - Classic visionOS moves both auxiliary rails into the bottom ornament
(2026-08-10): the ▤ rail rides above the UMD row in one slab, the ⌗ rail
becomes three slabs with the address readout as the window identity.
Inventory, sizing rules, and the resize-bar/anchor traps live in
input-and-windows.md; Shell/iPad keep the in-window rails.WKNavigationDelegatere-applies the allowlist per navigation (multiplex:never navigable; mailto re-presents the sheet); no JS bridge, no send path into any terminal. One app-scoped persistentWKWebsiteDataStoreshared by every viewport; Clear Browsing Data… on the readout's long-press menu wipes it globally after a confirmation. A viewport never claimsTerminalFocusArbiter(switching to ⌗ releases the previous responder). ATS is relaxed for web content only (NSAllowsArbitraryLoadsInWebContent); app networking keeps full ATS. Load failures render a chassis NO ROUTE panel naming the network. - The file viewer is the viewport's sibling for paths — and path presses
confirm instead of falling to selection (
TerminalRoute.Mode .fileViewer; pure models inModels/FileViewer/; records inlocal-plan/file-viewer-bakeoff/+local-plan/file-viewer.md). Two summons: + TAB ▸ File Viewer roots at the pane cwd ($HOME when no pane answers), and a pressed path (TerminalPathTarget;:12[:col]and tool-call-style:12-18[:col]suffixes ride as line targets) raisesTerminalFilePathSheet→ ▤ VIEW.file:///absolute/pathandfile://localhost/absolute/pathtake the same road: percent escapes decode to the REMOTE path, URI syntax proves spaces/trailing marks are filename bytes (no prose trimming), and query/fragment/non-local-authority shapes stay blocked + copy-only. This file-URI check MUST precedeTerminalLink, while ordinary links MUST precede ordinary paths soexample.com/docsstays a URL. Only text BOTH resolvers decline ($VAR/…, colon prose, whitespace in a bare-relative shape) still falls to selection. Paths with spaces resolve when rooted by a base marker (/,~/,./,$HOME/); bare relatives keep the prose guard.trimmingProseTailsheds trailing chunks carrying neither/nor.— hence the sheet's OPENS row (verbatim mono) whenever the resolved spelling differs from the field. Split panes: detection is pane-aware in the fork (a border-glyph row scopes matching + wrap-joining to the pressed pane's columns — seeswiftterm-fork.md), and a path press carries its screen cell so a relative path resolves against the PRESSED pane's cwd (TmuxProbe.parsePathAnchorDirectory/HerdrProbe .parsePaneWorkingDirectory, rect-containment with active/focused-pane fallback — a split's panes routinely sit in different repos). The cell lives inTerminalSessionController.pathPressScreenCell, overwritten per press rather than cleared with the sheet (the ▤ VIEW confirm reads it after dismissal), and rides the summon (anchorCell) so the mosh re-ask aims at the same pane; + TAB browse and the debug hooks pass no cell and keep the active-pane anchor. Wrapped-row glue: hard wraps leave no seam space, soLinkMatch .rowTextscarries per-row fragments (built only for multi-row matches; deliberately NOT via the OSC-8-authoredparamsdictionary) andWrappedRowGlue.cutTarget(pure + tested) cuts at the first seam whose butting chunk carries neither/nor.; the cut suffix resolves link-then-path with the join as fallback, so a wrong cut can never kill a working press. Accepted trade: a bare path whose whole first segment sat on the upper row (local-p⏎lan/x) cuts wrong — visible in the editable field. The fork's row-join heuristic (rows neverisWrapped) also refuses a finished-file⏎word seam (test.ts⏎modified, thegit statusshape) before any join — seeswiftterm-fork.md; the cut here handles seams that still join.strippingWrappedProseHeadis the textual fallback for seamless callers (edited field, gaze regions). visionOS gaze regions stay URL-only on purpose (hover regions are hit regions; build logs are walls of paths). Details: the viewer dials its own SSHConnection — never the probe's (a disabled host must not be revived) and never the tab's transport (merge/split moves the viewer) — redialed once per op after suspension; works for mosh hosts (SSH stays the control plane). A mosh summon carries its fullSessionKey, so that connection re-asks tmux's active pane or herdr's focused pane for the cwd before$HOME— a bare session name regressed herdr to home when backend support landed. SFTP for listings/bytes (structural, never parsels; reads fill fixed chunks concurrently — sequential chunk walks cost seconds per MB at real RTT). Exec for git: Citadel'sexecuteCommandTHROWS on nonzero exit, so every git command tailsprintf '\nMPXFV_EXIT:%s' "$?"(GitCommands.splitExit) — "not a repo" ≠ "empty diff", and--no-indexexits 1 routinely;-c core.quotepath=false,--no-ext-diff. Rendering is zero-dependency on purpose (2026 survey: no maintained pure-Swift highlighter exists):CodeHighlighter(line-oriented, carry state survives breaks — diff rows highlight per-side),MarkdownDocument(GFM subset),GitDiff/GitFileStatus, all fixture-tested; graduation seam is exactlyCodeHighlighter.highlight(_:language:). Code/diff screens are ONE selectable TextKit 2 view (FileViewerTextView, content assembled off-main): the text is exactly what a copy should carry; numbers, grounds, and washes are decor drawn by pinned companion views, unselectable by construction. Traps on record: theusingTextLayoutManager:convenience init crashes a Swift subclass (hand-build the stack), and apath:12/path:12-18centered scroll (the range starts centered and every requested row is washed) must defer one main-queue hop past first layout. Rendered markdown keeps SwiftUI blocks; a rail SELECT chip re-hosts the raw source on the selectable screen. A markdown image is a captioned placeholder that a press turns into the picture, in place (MarkdownInline.image(alt:destination:);FileViewerController.InlineImage): rendering a document still fetches nothing — the press is the only thing that does, and it SHOWS rather than navigates (the tab stays on the README). The caption is the switch:⟨image: alt⟩in link ink while hidden,⌄ image: altabove the picture once shown, pressed again to put it away. A web URL is not a file this viewer fetches — it goes to the link sheet exactly as it does in prose, with both ⌗ VIEWPORT and the external OPEN handoff — and a destination-less![alt]()stays the inert caption it always was. Load-bearing details:- Pictures ride INSIDE the block that names them
(
FileViewerMarkdownProseBlockView), never as extra rows of the document stack: that stack is index-paired withblocks, and mounting, restyling, and the reader's scroll anchor all count on it. - The height is a constraint re-derived from the width Auto Layout
actually gave the view, never a cached
intrinsicContentSize. The column's width arrives after the picture does, and one stale measurement leaves the picture floating in a band of empty chassis — shipped and caught on the sim, 2026-08-08. Fit is the column, capped by the picture's own pixels (never upscaled) and bymaximumHeight(one figure can't take the whole screen); tapping it opens the full ▤ screen, where zoom lives. - Decoded at
inlineImageMaxPixelEdge(1600, against the full screen's 4096) because a README shows many at once; keyed by the destination as written, so one fetch serves every repeat of it, and cleared when the screen moves to another document. FileTree.resolve(reference:from:)is the one resolver the picture and link roads share — percent escapes decode to the REMOTE path,#anchoris dropped (there is nothing to scroll to), an anchor-only reference resolves to nothing;MarkdownDocumentsheds the two decorations CommonMark allows around a target (a"title"and<…>brackets) so a press can't aim at a path the document never named.- A picture this screen can't draw (an SVG, a PDF, a stat failure) says
so where it would have been, with an OPEN FILE chip onto the ▤ screen
— a press is never a dead end. Table cells keep the old road outright
(a picture would wreck the measured column widths). Honesty rules: NUL-sniff says BINARY; >1.5 MB renders its head
under TRUNCATED; a deleted file's row opens its diff; failures name the
cause. A browse summon starts the drawer OPEN (the tree is the subject
until a file is chosen).
FileTree.hiddenNameshides only the editor-default set (.git/.svn/.hg/CVS/.DS_Store/Thumbs.db — NOT all dotfiles; content untouched). Watching is polling, the deck's way (never a remote inotify/fswatch — nothing long-running is assumed onto the host): active tab +applicationState == .active→ a 5 s tick runs ONE combined git exec (GitCommands.watchProbe/parseWatchProbe) plus one SFTP stat; expanded listings sweep every third tick. Results land as QUIET swaps (no .loading, no scroll reset) and acontentGenerationcounter drops stale results. Known blind spot: a net-zero-delta edit under an unchanged porcelain line escapes the repo-wide diff until REFRESH; a vanished watched document flips to FILE GONE, transport blips change nothing. Shared auxiliary-pane rules with the viewport: no tally dot, no focus claim,syncTabsstrips controller-less tabs and must NEVER mint aTerminalSessionControllerfor an auxiliary route. Reading size is a quantized ladder, app-wide and device-local (FileViewerTextScalepure + tested;FileViewerTextScaleStore, theThemeStoreprecedent): it multiplies the authored size,Theme.typeScalestill last. A pinch snaps to a rung and writes only on a change (FileViewerTextScalePinch— a rung costs a rebuild, so never a continuous multiplier); A− / A+ ride the tab's UMD rail (ViewportUMDConfiguration.textScale) on EVERY platform — Designed-for-iPad has no pinch — with the percentage in front of them, readout and reset, only off 100%; a ⌗ viewport tab passes nil. The pane reads the store inside its observation, so one pinch resizes every open ▤ tab, and the size stays out ofBodyKey: the live screen rebuilds in place, scroll position and selection with it. Three measured rules keep that rebuild off a second (iPad sim, 2026-08-06). Clear a TextKit view before refilling it (FileViewerTextView.setContent): assigning over a populated document makes TextKit reconcile the two — 1.4 s for 8 000 lines against 1 + 10 ms, and every screen swap pays it, watch ticks included. A rendered-markdown resize restyles mounted blocks in place, near the viewport first (restyleNearViewport/restyleAhead), the rest catching up as they are scrolled toward with the anchor block held still — remounting costs the whole scroll depth (2.7 s for 160 blocks: every block above the reader is rebuilt before theirs has a position). Only fences and tables are rebuilt, their geometry being measured into constraints.FileViewerMarkdownTextViewcaches its height per width — a stack view asks EVERY arranged subview for its intrinsic size on any layout pass, so one restyled block re-ran CoreText over the whole screen. ⚠mountBlocksIfNeededmustsetNeedsLayoutthe scroll view before readingcontentSize, or a teardown's stale tall height reads as a full viewport and the screen stays BLANK until the reader scrolls.
- Pictures ride INSIDE the block that names them
(
- The side panel is the first home for a terminal-summoned ▤/⌗ on iPad and
visionOS (2026-08-21;
SidePanelViewController,SidePanelPolicy,SidePanelWidth): an overlay over the pane on regular-width iPad (≥ 660 pt) and a trailing ornament on visionOS, so SwiftTerm never receives a resize. One panel per host terminal tab — it follows the tab through merge/split, hides without stopping behind another tab, and a second summon replaces and shuts down the first. Path/link confirmations and external file actions on an active terminal use it; + TAB ▸ File Viewer, tree-row "open in new tab", summons anchored to an auxiliary tab, and README links from ▤ stay on the tab road. ↗ TAB re-registers the live controller as an auxiliary tab (routeMode); closing the panel or host tab shuts it down; panels are never restored; a Stage Manager shrink below the floor makes the same panel→tab move. Width is device-local and dragged live (iPad 320 … pane − 320; visionOS from a handle on either edge, stored glass-relative). DEBUGMULTIPLEX_SIDE_PANEL=0forces the tab road. - PDFs and sound files are screens, not BINARY (2026-08-15;
FileRenderKind.pdf/.audio,FileViewerPDFContentView,FileViewerAudioContentView,FileViewerAudioClip). Both are read WHOLE over SFTP under one 60 MBmediaByteLimit(readWhole, shared with images; TOO LARGE names the cap) — PDFKit wants random access and there is no streaming road over SFTP. Video containers stay.binary.- PDF = PDFKit's
PDFViewwith a PAGE n / m badge and the image screen's%·FITchip (FileViewerZoomReadout). ⚠ ANY write tominScaleFactor/maxScaleFactor/scaleFactor(a pinch included) switchesautoScalesOFF, after which a resize no longer refits (measured on the visionOS sim); so nothing sets the bounds and FIT re-armsautoScalesinstead of assigning a scale. A URL link inside a PDF is untrusted document text:pdfViewWillClick(onLink:with:)is implemented because PDFKit otherwise opens the URL itself — it ridesopenMarkdownLinkinto the link sheet. A quiet watch swap keeps the reader's page (LaTeX build loops). Locked PDFs get a LOCKED panel with UNLOCK… (system secure field, cleared in every button action, password never kept); PDFKit unlocks IN PLACE, sounlockPDFre-publishes the sameDocumentand the pane rebuilds only becauseBodyKeyflipspdfLocked → pdf. - The audio clip belongs to the
Document, the screen is its remote (Document.audio, anAVAudioPlayerover the bytes): merge/split rebuilds the pane, not the document, so the position survives; a quiet reload hands position + playing state to the replacement (adoptPosition);shutdown()silences it. ⚠ Build the player ON the main actor:AVAudioPlayer(data:)insideTask.detachedmeasured 100–400× slower on the visionOS sim (~9 s for a mismatched hint). The screen polls at 4 Hz only while playing;UIKitChassisChip.setContentandFileViewerBadgeView.setTextgate themselves, so readouts may be set from timers and layout. The document leaving the screen pauses, hiding it does not:FileViewerController.content'sdidSetpauses a clip whose document left (another file, a diff, a failure); a hidden tab keeps playing, and merge/split never touchescontent. The.playbacksession is claimed only at PLAY and handed back on pause / finish / release. A decode failure keeps the AUDIO verdict (CAN'T PLAY panel; the Ogg family is admitted on purpose so.opusplays and Vorbis fails honestly). Volume is the listener's, not the file's (FileViewerAudioVolume+FileViewerAudioVolumeStore, the text-size store's shape: device-local, never synced): every clip and every panel follow the store throughwithObservationTracking(its hook fires before the write lands — hence the main-actor hop before re-reading), and the slider only writes the store, in whole percents. No background-audio mode is claimed.
- PDF = PDFKit's
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.
- 2d ago First seen · 301 lines · 0 tokens per session scan A 517cc3b64f8d
links-and-viewers is an agent published in the GitHub repository multiplex-term/Multiplex (10 stars, last pushed 6d ago), licensed Apache-2.0. It costs nothing until one of its globs matches a file; then it loads 5,247 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.
Other agents, from other repositories
multi-server-state-isolation
Which app state is per-server versus intentionally global, where each lives, and which tests guard the isolation. Almost all isolation holds by construction through the server-keyed view tree and cache keys described below; "Clear Offline Cache" is explicitly scoped to the active server.
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.
kanban
Durable rules for the shipped Kanban feature (HermesMobile/Features/Kanban/). These are normative where they differ from the desktop WebUI. Vocabulary is owned by root CONTEXT.md: upstream task/taskid stay network-boundary names; user-facing and Swift domain names use Card with a Kanban qualifier.
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…
triage-labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
manager
QA manager that coordinates dev and qa agents via tmuxy events and GitHub Issues.