mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-03 00:01:55 +00:00
* feat(search): replace the command palette with tabbed Search Everywhere The palette was one flat list that every new kind of row had to be squeezed into: tabs and SSH hosts rode along as "Switch to Tab: …" and "SSH: …" commands, and the only way to narrow to one kind was a magic seed word. Search Everywhere splits it into sources behind one trait — All, Actions, Terminals, Hosts — each with its own empty-query layout and ranking. The All tab shows each source's top rows, ordered by best match, with a row that opens the full tab. Tab / Shift-Tab walk the tabs and keep the query. - Terminals lists every open tab of every workspace (reusing the switcher's tab rows) and jumps to it wherever it lives, plus the shells and agents. - Hosts replaces the separate "Add Connection" input: a typed address or full `ssh …` line offers to connect. - Fixes Return doing nothing after a search that found nothing, or when the search opens pre-filtered: gpui-component re-picks the row from a stale frame; the delegate now re-arms the first row. - Fixes `ssh -p 2222 me@box` being offered as a quick-connect address with user `ssh -p 2222 me`. The keymap action stays `TogglePalette` so custom bindings keep working. * feat(search): a Sessions tab to resume past agent sessions Search Everywhere gains a Sessions tab listing the Claude Code and Codex sessions on this computer, read from ~/.claude/projects and ~/.codex/sessions ($CODEX_HOME). Sessions that ran in the focused tab's directory lead; the All tab offers the last three of them before anything is typed. Return opens a new tab in the session's directory and runs the agent's resume command with its configured launch flags. - Only each transcript's head and tail are read, off the window thread, and cached by path, size and mtime: ~170ms cold for 50 sessions, under 1ms warm. The search opens on the cached list and fills in. - Titles: /rename name, then the agent's own title (ai-title, Codex's session_index.jsonl), then the first thing typed, skipping harness injections. Codex rollouts it ran for itself (subagent/internal) and sessions never asked anything are left out. - A session whose directory is gone is refused with a notice rather than resumed where the agent cannot find it.
217 lines
7.8 KiB
Plaintext
217 lines
7.8 KiB
Plaintext
---
|
|
title: "SSH"
|
|
description: "A native Rust SSH stack: quick connects, saved profiles, keychain credentials, jump hosts."
|
|
---
|
|
|
|
tty7 speaks SSH itself, over [russh](https://github.com/Eugeny/russh). It never
|
|
shells out to the `ssh` binary, and there is no compatibility mode that does.
|
|
|
|
That is what makes the rest possible: credentials in the OS keychain,
|
|
[SFTP](/remote/sftp) in a side panel, [port forwards](/remote/port-forwarding)
|
|
you can add mid-session, and authentication prompts drawn as sheets in the pane
|
|
instead of a password echoing into your shell.
|
|
|
|
<Frame caption="A key passphrase asked for as a sheet inside the pane, with the option to keep it in the keychain">
|
|
<img src="/images/ssh-passphrase.webp" alt="Connecting over SSH in tty7" />
|
|
</Frame>
|
|
|
|
## Four ways to connect
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="QuickConnect — type an address">
|
|
Open Search Everywhere (<kbd>⌘ P</kbd>) and type an address. IPv6 works with
|
|
brackets.
|
|
|
|
```
|
|
me@devbox
|
|
me@devbox:2222
|
|
me@[2001:db8::1]:22
|
|
```
|
|
</Accordion>
|
|
|
|
<Accordion title="A saved profile">
|
|
Profiles live in **Settings → SSH → Hosts**. Start typing the name in
|
|
Search Everywhere's Hosts tab, or open the *SSH: Manage Profiles…* command.
|
|
</Accordion>
|
|
|
|
<Accordion title="An alias from ~/.ssh/config">
|
|
Type an alias you already have and tty7 resolves it natively — common fields,
|
|
best effort — then connects over russh. **Settings → SSH → Import from
|
|
~/.ssh/config** turns aliases into real profiles.
|
|
|
|
<Note>
|
|
`Match`, `canonicalize*`, and GSSAPI directives are not supported, and
|
|
there is no fallback to the system `ssh` when one appears.
|
|
</Note>
|
|
</Accordion>
|
|
|
|
<Accordion title="A remote workspace">
|
|
The same connection can host whole workspaces on the far machine rather than
|
|
a single shell. [Remote workspaces →](/remote/workspaces)
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
## Profiles
|
|
|
|
**Settings → SSH → Hosts** holds the full connection config. The basics:
|
|
|
|
| Field | |
|
|
|---|---|
|
|
| **Name** | A label for this connection |
|
|
| **Host** | Hostname or IP |
|
|
| **User** | Login user — blank resolves at connect time |
|
|
| **Auth** | *Auto* (tries every applicable method), *GSSAPI*, *Password*, *Key*, *Agent*, or *2FA* |
|
|
| **Jump host** | Another profile, or a `ProxyJump` chain |
|
|
| **Port forwarding** | Rules opened with the connection |
|
|
|
|
**Defaults** at the top of the list is inherited by every host, so a setting you
|
|
want everywhere is set once.
|
|
|
|
Right-clicking an SSH tab opens that connection's host form — **Edit Host…**
|
|
for a saved one, **Save as SSH Host…** for an address typed by hand. It is the
|
|
same row the workspace switcher's machine menu carries, so a hostname or
|
|
password typed wrong is corrected from the tab you noticed it on. The menu acts
|
|
on the tab it was opened on, not on whichever pane is focused. Saving one opens
|
|
on the whole live connection — its proxy, keys and forwards as well as its
|
|
address — so the host that lands is the one you were already on.
|
|
|
|
Saved hosts are kept in `servers.json`, beside `config.json` rather than in
|
|
it, so a `config.json` you sync between machines does not carry them.
|
|
Passwords and key passphrases go in the **OS keychain**, never in either file
|
|
and never on disk in plain text. **Forget Password** in a
|
|
profile's menu removes the stored one.
|
|
|
|
Deleting a profile drops its keychain credentials and forgets the remote
|
|
workspace entries that connected through it — the confirmation counts them
|
|
first. The sessions on the machine itself keep running; [what happens to its
|
|
entries →](/remote/workspaces#deleting-a-profile)
|
|
|
|
### Advanced
|
|
|
|
Behind **Advanced** on a profile, grouped:
|
|
|
|
| Group | Fields |
|
|
|---|---|
|
|
| **Authentication** | Identity files (one path per line, `%h`/`%r` expand), agent forwarding |
|
|
| **Proxies** | ProxyCommand (`%h`/`%p`/`%r` substituted), SOCKS5 proxy, HTTP proxy |
|
|
| **Algorithms** | KEX algorithms, ciphers, MACs, host-key algorithms, compression |
|
|
| **Connection** | Keepalive interval and count, connect timeout, X11 forwarding |
|
|
| **Session** | Shell integration, login scripts, skip banner |
|
|
| **Security** | Host-key verification, remote clipboard image writes |
|
|
|
|
Everything blank means "the library default", so you only fill in what you
|
|
actually need to override.
|
|
|
|
## Copying a remote image to this machine
|
|
|
|
Programs on an SSH host can write PNG, JPEG, GIF, or WebP images to the system
|
|
clipboard on the machine running tty7 with the OSC 5522 clipboard protocol.
|
|
Enable **Advanced → Security → Remote clipboard images** for that saved host
|
|
first. It is off by default because any program that writes terminal output
|
|
would otherwise be able to replace the clipboard.
|
|
|
|
This Python script can be installed on the remote host as
|
|
`tty7-copy-image`:
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
import base64
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import select
|
|
import secrets
|
|
import sys
|
|
import termios
|
|
import time
|
|
import tty
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
mime = {
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".gif": "image/gif",
|
|
".webp": "image/webp",
|
|
}.get(path.suffix.lower())
|
|
if mime is None:
|
|
raise SystemExit("supported formats: png, jpg, jpeg, gif, webp")
|
|
|
|
data = path.read_bytes()
|
|
if len(data) > 16 * 1024 * 1024:
|
|
raise SystemExit("image exceeds tty7's 16 MiB clipboard limit")
|
|
|
|
osc, st = b"\x1b]5522;", b"\x1b\\"
|
|
encoded_mime = base64.b64encode(mime.encode())
|
|
request_id = secrets.token_hex(8)
|
|
out = sys.stdout.buffer
|
|
fd = sys.stdin.fileno()
|
|
old = termios.tcgetattr(fd)
|
|
status = None
|
|
try:
|
|
tty.setraw(fd)
|
|
rid = request_id.encode()
|
|
out.write(osc + b"type=write:id=" + rid + st)
|
|
for offset in range(0, len(data), 4096):
|
|
chunk = base64.b64encode(data[offset:offset + 4096])
|
|
out.write(
|
|
osc + b"type=wdata:id=" + rid + b":mime=" + encoded_mime + b";" + chunk + st
|
|
)
|
|
out.write(osc + b"type=wdata:id=" + rid + st)
|
|
out.flush()
|
|
|
|
reply = bytearray()
|
|
pattern = re.compile(
|
|
rb"\x1b\]5522;type=write:status=([A-Z]+):id=" + rid + rb"\x1b\\"
|
|
)
|
|
deadline = time.monotonic() + 5
|
|
while time.monotonic() < deadline:
|
|
ready, _, _ = select.select([fd], [], [], deadline - time.monotonic())
|
|
if not ready:
|
|
break
|
|
reply.extend(os.read(fd, 4096))
|
|
match = pattern.search(reply)
|
|
if match:
|
|
status = match.group(1).decode()
|
|
break
|
|
finally:
|
|
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
|
|
if status != "DONE":
|
|
raise SystemExit(f"clipboard write failed: {status or 'timeout'}")
|
|
```
|
|
|
|
Run `tty7-copy-image screenshot.png`. A compliant sender may include an OSC
|
|
5522 request id and wait for tty7's `DONE`, `EPERM`, `EINVAL`, or `ENOSYS`
|
|
response. Clipboard control packets are not retained in scrollback and are not
|
|
replayed after reconnecting.
|
|
|
|
## Authentication prompts
|
|
|
|
Password, key passphrase, and 2FA prompts appear as sheets inside the pane, with
|
|
a **Remember (keychain)** option where it makes sense.
|
|
|
|
## Host keys
|
|
|
|
Host keys are verified against `known_hosts` by default. A first connection asks
|
|
you to confirm the fingerprint; a **changed** key is a much louder prompt that
|
|
makes you type `yes` to override, because that is what a changed key deserves.
|
|
|
|
**Settings → SSH → Security → Verify host keys** turns verification off
|
|
entirely. It is on for a reason.
|
|
|
|
Also under Security: **Warn before closing** a live connection, off by default.
|
|
|
|
## Reconnecting
|
|
|
|
<kbd>⌘ ⇧ R</kbd> — or *SSH: Reconnect* in Search Everywhere — restarts the session in
|
|
the current pane. Useful after a laptop sleeps or a network changes.
|
|
|
|
## What is not supported
|
|
|
|
- No fallback to the system `ssh` binary
|
|
- No `Match` or `canonicalize*` directives from `~/.ssh/config`
|
|
- No GSSAPI *directives* from `~/.ssh/config`. Kerberos `gssapi-with-mic` itself
|
|
is supported — pick **GSSAPI** in a profile's Auth field — it is just not
|
|
something the config-file resolution path reads
|