Back to the dev blog Release notes · iPhone & iPad

SharkTTY 1.8.1: it listens now

· Safafish

The App Store note for 1.8.1 is four lines about the virtual mouse. The build behind it is nine days of work and the largest single jump the iPhone and iPad app has had this year: voice input with an optional AI layer, a keyboard rebuilt around real layout data, a client for a third terminal protocol, share links for a Mac, and a sync format designed to survive its own future versions. This is the long version.

It listens now

Typing a shell command on a phone is the worst part of using a terminal on a phone. So 1.8.1 added voice input, built in stages over about a week.

The first stage is the part that matters most and costs nothing: hold the mic, talk, and recognition runs on the device. Free, offline, and nothing leaves the phone. What it heard lands in a voice board as editable text, and you insert it into the terminal when it looks right — not before.

The second stage is about the times it does not look right. Recordings are kept as segments, so you can trim one and re-recognize just that part rather than repeating the whole sentence, and a history keeps past recordings for playback and another attempt. On iPad the voice board floats and zooms the way the transparent keyboard does, because on a large screen a fixed panel in the wrong place is its own problem.

And, if you want, it thinks

Recognition gives you words. A command needs more than words — it needs flags in the right order and no spaces where there should not be any. The AI layer in 1.8.1 is entirely optional and entirely separate from recognition.

Bring your own provider: the OpenAI, Claude, Gemini, Kimi and GLM API formats are supported, and you set a chat model plus, if that provider has one, a transcription model. Claude is chat-only and has no audio API, so recordings there fall back to on-device recognition rather than failing. Provider keys live in this device's Keychain and are deliberately excluded from sync.

With a provider connected, prompt modes turn what you said into clean text or into a command you can run, and an option to attach terminal context lets the model see what is actually on screen before it answers. There is also a chat panel, so the question you would otherwise have asked in a browser tab can be asked next to the session.

For people who would rather not manage an API key, official cloud models are available after signing in, paid for with Points bought through in-app purchase and shared across your devices. On-device recognition stays free either way, and that is the default.

A keyboard with real layouts

The floating keyboard through 1.8 was one hard-coded US layout. 1.8.1 made it data-driven and shipped six: US English, UK English, Japanese (JIS), French (AZERTY), German (QWERTZ) and Spanish, switchable in settings along with the choice of board.

Data-driven layouts need a test that hard-coded ones do not, because a layout is now a table somebody typed. We added a character-reachability test — for every layout, is every character you would expect actually reachable through some combination of keys? It immediately found two real bugs: AZERTY had no * at all, and the JIS layout anchored fn to the wrong key so the F row was off. Neither would have been caught by anyone testing in English.

Above the keys there is now a candidate strip backed by a word suggester tuned for terminal context and common commands, and the mic moved into the top bar with two behaviours: tap it to switch to the voice board, hold it to push to talk straight into the candidate strip. That hold gesture turned out to be harder than it looks — SwiftUI's obvious long-press API cannot tell you when the finger lifts, which we wrote up separately in a post about hold-to-record buttons.

A third way in: Eternal Terminal

Mosh is what keeps a session alive through a train tunnel, and it runs over UDP. Plenty of corporate and hotel networks pass TCP and nothing else, which leaves those users with plain SSH and no session persistence at all. Eternal Terminal solves the same problem over TCP, and many people already run etserver. We wrote about the client the week before; 1.8.1 is the build it landed in.

It arrived the way an experimental transport should: an isolated Rust crate first, with nothing else able to depend on it; then wired through the FFI into the Swift app behind a flag; then, once it worked, a real host-editor UI, a settings toggle, error messages that name the actual cause, and translations. Two rounds of review followed on the failure paths, which is where a network client is most likely to be quietly wrong.

It remains off by default. Turn on the ET transport in Settings and it appears as a third connection method next to SSH and Mosh. One thing worth planning around: like Mosh's UDP, the ET data connection goes straight from the device to the etserver port and is not forwarded through a jump host.

Config that survives its own future

The Mac agent grew its own host list and terminal in this period, which meant the phone and the Mac needed to agree on what a host is. 1.8.1 introduced a shared package holding the canonical host and script schema, used by both, plus a config-sync client and the coordinator that drives it.

Sharing a schema across two apps that update independently creates a specific hazard, and we hit it: an older client decoding a value it has never heard of, then writing the whole record back and destroying it. The fix — enums that preserve their raw value, a bag for unknown fields, and tests that pin the contract — is written up in Forward-compatible Codable. In this build that pattern was applied to hosts first and then to every remaining synced model, including the per-entry unknown fields on saved desktops.

What syncs and what does not stayed unchanged and explicit: hosts, scripts, drive mappings, templates and settings sync to the server under your account. Passwords, SSH private keys and their passphrases never leave the device — they live in the iOS Keychain, optionally travelling end to end through iCloud Keychain, with nothing on the server to compromise.

Sharing a Mac with someone else

1.8.1 added the viewer side of share links: a Shared with Me list, transport support for connecting through a share rather than a pairing string, and handling for sessions that are gated behind the owner's approval. The errors are specific on purpose — an expired link, a revoked link, all concurrent seats taken, an account that is not on the allowed list and a connection outside a booked window all say which of those things happened, because “could not connect” is useless when the fix depends on the reason.

Also here: disconnecting a remote desktop keeps its tab, the way an SSH connection always has, so you reconnect from the overlay instead of setting the whole thing up again.

The virtual mouse gets somewhere to live

This is the part the App Store note describes. The mouse buttons could be split and placed, which was flexible and meant they were never quite where your hand was. Now they dock: one panel along the bottom of the screen that slides left or right to wherever you hold the device, and rides up above the on-screen keyboard whenever one is open.

Docking also fixed the magnifier's worst habit. Free-floating, it had to dodge screen edges, which meant it moved when you did not expect it to and the magnified view did not line up with the cursor. Docked, it sits directly above the cursor and stays there. Two related fixes: sliding a finger off the edge of the screen no longer makes the cursor jump or leaves a button stuck down — a cancelled touch is now handled as a cancelled touch — and ⌘, ⌥ and ⌃ no longer occasionally stay pressed and turn your next keystroke into a surprise shortcut.

A bell you can hear from another app

When a terminal rings its bell and that session is not the one you are looking at, iOS now posts a notification. Append printf '\a' to a long build and your phone tells you when it is done. Bells from the session you are actually watching stay silent, which is the whole trick.

Housekeeping worth mentioning

  • A full localization audit at the end of the keyboard work: 639 keys checked, zero duplicates left.
  • Two correctness fixes from review: a race in decoder state, and Keychain passwords that could be orphaned when a host went away.
  • The Opus audio path from 1.8 finished here — a buffered encoder producing 20 ms frames from arbitrary PCM, playback on the viewer, and a fix for reading interleaved PCM out of the audio buffer list.
  • The test targets moved down to iOS 17.0 to match the app itself, so tests run everywhere the app does.

All of it is free. On-device voice recognition included.

Read this post in: English · 中文 · 日本語 · Español · Français · 한국어 · Українська

More on the app itself in the SharkTTY guide, the short user-facing notes in What's New, and the fastest way to reach us is the feedback board. Back to the dev blog.