A remote workspace's ports were never listed. The pane lives in the
peer's registry and QueryProcs asks this machine's daemon, which has
never heard of it, so the answer was an empty list — indistinguishable
on screen from a pane serving nothing. Add a control request so the peer
answers instead, gated on a feature so an older server says "I cannot
tell you" rather than "nothing is listening".
With the ports visible, the forward becomes something the user should
not have to think about: a port opens on a click, and a new one is
forwarded unasked, at the same number where that number is free here.
The watch runs with the panel shut, which is when a port appearing is
most worth saying something about.
Ports and Forwards were two sections that never mentioned each other; a
row is now a port, and the forward is where that row says it comes out.
Adding one by hand asks for one number instead of five fields, with the
rest of the ssh -L grammar one disclosure away.
Claude-Session: https://claude.ai/code/session_01TPXrptp2rCGKjXaz4xE3Lq
* feat(ssh): allow remote image clipboard writes
* fix(ssh): keep a profile's clipboard grant across a re-attach
A native ssh pane's OSC 5522 permission is decided by the spec that
dialled the host, and the daemon is the only side that holds it. A window
reopening onto a pane that outlived it attaches by pane id, has no spec
to read, and sends `allow_remote_clipboard_write: false` — which the
daemon took as the new answer and the pane's own view took as a refusal.
Both sides then said no, so the first restart after switching the
permission on turned every copy into an `EPERM` with the switch still
reading "on".
Pin the spec's answer in the pane and route both attach and detach
through one decision point, so a pane that carries a spec keeps that
spec's answer whatever an attaching client claims, and a pane without one
— everything on a remote `tty7-server` — is exactly as permitted as its
controller says. On the client side, refuse only what the pane can see is
forbidden and leave the verdict to the daemon otherwise.
Also: release a failed transfer's buffered bytes instead of parking up to
`MAX_CLIPBOARD_BYTES` per pane until the next request, and answer the
capability probe with the permission actually in force rather than a
constant that always reads as "off".
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
A click on a file in the SSH Files panel used to start a download; the
only way to change a remote file was download, edit, re-upload. Now a
click opens it in the built-in editor and Cmd-S saves straight back over
the pane's own SFTP channel, matching what the Files panel already does
locally and over a remote workspace.
- protocol: SftpOp::ReadFile/WriteFile and SftpOpResult::File, bytes as
base64; the reply carries the body plus the stat it was read under
- daemon: ReadFile enforces the caller's size ceiling before and during
the read; WriteFile rewrites in place (truncate, not temp-and-rename)
so the file keeps its mode and ownership
- SftpHost: a Host over the pane's SFTP route, so the editor's existing
open/save path works unchanged; git/search/watch honestly Unsupported
- editor: an open buffer holds the host it was read from, and
save/reload/dedup/watch key on (host, path) instead of the active host
- panel: single click opens (dirs navigate, text files edit), the same
gesture as the local tree; binary or oversized files get the local
tree's toast, and Download moves to the context menu
Review follow-ups, in this PR: the SFTP host stays out of HostRegistry,
which means "a machine this window has a link to" and is swept as such —
filing the pane's channel there made Cmd-S return silently once a
workspace deletion took it back out. The cursor-jump lookup, the status
bar's path, and the SCM panel's repository all key on the buffer's own
host now. Closes#656.
* feat(switcher): flatten the workspace list, add a create form, sync remote listings on connect
The switcher's left column is now one flat most-recently-used list across
all machines, each row carrying its machine and link state; the per-machine
tree, headers, and the Other Machines band are gone. Machine trouble
(install progress, connect errors, parked routes) moves to contextual
banners under the list, and machine verbs move into each row's menu.
Cmd+Shift+N now opens a create form instead of silently swapping the
workspace: a name prefilled with the usual generated codename, and a host
combobox (searchable dropdown) defaulting to this computer. Creating on a
machine with no live link connects first and completes when the link is up.
Connecting to a machine also mirrors its workspace listing into the local
store, so its workspaces survive a restart without a connection. Mirrored
references are marked synced: launch restore skips them, and their clock
follows the machine only until this client opens them.
* fix(switcher): drop a parked create when its machine's connect is called off
Disconnect clears the in-flight connect, so finish_connect never runs and
the PendingCreate outlived the intent behind it: the next successful
connect to that machine would have silently created a workspace nobody
was waiting for anymore.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(control): bump the dialect to v6 so an out-of-date server says so
The control dialect has been renamed, extended and cut since it was
last numbered, all of it against CONTROL_VERSION 5: the machine tree
replaced WorkspaceList/Get/Put/Delete with WorkspaceTree, MachineGet
and the tab/pane verbs, GitStream arrived with its chunk and end
events, and ReplyOk::Attached and FileMeta went away.
A peer left behind by any of that still answers the hello, because the
number it answers with still matches. It is also still sitting at the
path the installer looks for, tty7-server-c5p5, so a client decides it
already has the server it needs. Then the first call reaches a variant
the peer has never heard of, the frame fails to decode, and the read
loop takes the whole link down with it. What the user sees is a remote
workspace that opens with no tabs and a git detail pane that never
fills, with nothing anywhere saying why.
Moving the number puts all three guards back: the hello is refused with
the message that names the old build, the remote binary is looked for
at c6p5 and installed rather than trusted, and a stale local daemon
gets the restart prompt it should have been getting all along.
Document the rule next to the constant while it is fresh: move it when
a variant is added or removed. The feature strings only cover what a
peer can safely ignore, and a request it cannot decode is not that.
* feat(remote): publish a tty7-server for macOS hosts
A remote workspace has been Linux-only for no reason anyone chose: the
installer derives the asset name from `uname -sm`, and the only names it
knew were the two musl builds. A Mac on the other end of an SSH profile
got "a remote tty7 workspace needs a Linux host" and stopped there.
Publish the two Apple slices alongside them and teach the installer to
ask for them. `Darwin arm64` and `Darwin x86_64` now map to
tty7-server-macos-aarch64 and tty7-server-macos-x86_64; everything past
that point already worked, because nothing under it was ever Linux-
specific — the install path is POSIX, the upload is SFTP, and the
dialect probe runs the binary before trusting it.
The machine names are matched per system rather than by architecture
alone. Linux says aarch64 on one distribution and arm64 on the next,
while a Mac only ever says arm64, so honouring Linux's spellings under
Darwin would be guessing at output no Mac produces.
Static linking is not the instrument on macOS — Apple ships no static
libSystem — so assert-macho.sh stands in for assert-static.sh with the
guarantee that actually matters: every dependency resolves under
/usr/lib or /System/Library, so nothing the destination Mac lacks can be
picked up from a build runner, and the binary carries the signature
arm64 refuses to run without.
Not signed or notarized beyond that, deliberately. The binary is never
downloaded by the Mac that runs it: the client fetches it, verifies it
against checksums.txt and writes it over SFTP, which sets no quarantine
attribute, so Gatekeeper is not in the path.
ASSET_X86_64 and ASSET_AARCH64 become ASSET_LINUX_*, which is what they
always meant and could not keep meaning next to a macOS pair.
* fix(ci): sign the x86_64 macOS server, and stop the guard flaking on it
Two faults the first green run hid from each other.
The linker ad-hoc signs the arm64 slice because Apple Silicon will not
execute anything unsigned, and leaves x86_64 bare. That is fine on an
Intel Mac, but the x86_64 server is also what an Apple Silicon box gets
when it asks through a Rosetta shell, and handing that machine an
unsigned binary is a guess about Rosetta nobody needs to make. Sign both
slices ad-hoc in the workflow — no identity, no secrets, nothing to do
with the notarized signing the GUI bundles get.
The guard that caught it was itself unreliable: `codesign -dv | grep -q`
under `pipefail` reports failure whenever grep wins the race, because -q
exits on the first match and the writer takes SIGPIPE. Small output means
the writer usually finishes first, which is why the arm64 job passed and
x86_64 failed on the same signed-or-not question. Capture into a variable
and match afterwards, the way the release workflow already does it.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Deleting an SSH profile used to leave every remote workspace entry that had
connected through it behind, labelled with a bare internal id and retrying a
route that could never work again.
`RemoteRef` now carries a `RouteSnapshot` of the profile it was made from —
name, user, host, port — written at creation and refreshed on every reopen,
`serde(default)` so older session files load. The snapshot serves labels only:
`PartialEq`/`Hash` ignore it, or a refresh would split one entry into two.
Deleting a profile cascade-forgets the entries routing through it. Forgets,
not deletes: `WorkspaceRemove` is never sent, so the sessions on the remote
machine keep running and connecting again under a new profile brings them
back from the machine's own workspace list. An entry holding a live or
in-flight link is left alone, as is one whose window is still on screen — a
window whose workspace the store has forgotten reads as local, and its next
tab would open a local shell on what the user still sees as a remote box.
Whatever survives parks instead: no retries, no error, and an inline action
to drop it deliberately. A live or preempted link outranks a lost route.
Labels fall back from the live profile to the snapshot to a placeholder, so
no branch renders a bare UUID. Resolving the live name reads memory rather
than reparsing `~/.ssh/config`, because that path runs on every frame of a
window with a remote workspace open.
Closes#485.
Audited every page under docs/ against the source. Fixes for what the
code actually does:
- agents: the status vocabulary is idle/working/waiting/done, not
running/waiting/idle; hook rows grow a separate Uninstall button; the
Settings table labels read "Copilot CLI" and "Grok Build"; Copy Session
ID lives in the tab's context menu, not the pane's
- cli: `pane ls --all` reports the owning workspace id, not "tty7-cli";
document bare `tty7 [PATH]` as the GUI launcher it is instead of listing
it as unimplemented; note `active_tab` and the `diagnostics` array; wait
also defaults to $TTY7_PANE
- git: the branch dropdown is a plain list with no search box and no
stash-and-switch, and checkout is not a palette command; quote the diff
overlay's own overflow notice rather than the sidebar's
- window: the unread marker tracks a finished agent turn, not any output;
rows cannot be dragged across groups; the sidebar and `tty7 tab ls`
resolve labels differently; drop Toggle Commit History and Checkout to
from the palette's Git group; ~/.ssh/config aliases are not palette
entries
- terminal: Ctrl+R dedups by command text and shows no directory; Esc does
not dismiss a ghost suggestion; document Cmd+Enter
- remote: GSSAPI is an ordinary Auth choice, not a managed-connection-only
mechanism
- fonts: Maple Mono NF CN leads the chain on Windows and Linux only; list
the real per-platform defaults
- settings paths: the three Links settings and per-pane history were filed
under the wrong sections
38 pages under docs/, written against the source rather than the README:
config keys and their clamps from core::config, default keybindings from
ui::keymap, every CLI verb and flag from tty7-cli, agent aliases and
hook/fork/resume support from core::cli_agent, and Settings paths taken
from the actual en-US strings.
docs/features.md and its zh-CN translation are retired — everything in
them now lives in a page of its own, plus the two things they carried
that nothing else did (IME input, the performance notes). README and
README.zh-CN point at docs/ instead.
Screenshots and videos are placeholders for now: docs/images/placeholder.svg
with a caption naming what each shot should be.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>