Repository navigation
Close the remaining product gaps: one socket bridge, Antigravity dialogs, encrypted keys, accessibility - #177
Conversation
|
Thanks for this whole stack. We've reviewed #172 to #177 and want to take them, with a few changes. First, a heads-up: main is about to change. Our branch, shipped as TestFlight builds 87 to 89, goes in shortly. It removes the machine feature entirely: every computer is now its own saved host, and Chats lists every saved host's chats in one list. It also adds Files, Terminal and Computers tabs. It touches many of the files this stack changes ( Changes we'd like while you do:
Before merging, we'll check the iOS and Android CI builds, run the e2e suite in Dark and Light, and try the socket bridge on our own hosts. |
844c176 to
6bdb0d5
Compare
22a707b to
04194ef
Compare
04194ef to
60f3e32
Compare
6bdb0d5 to
025d543
Compare
|
Rebased #172 to #177 onto
One addition, Checked on the rebased stack: tsc, lint, jest and the Python tests on every branch; |
cobanov
left a comment
There was a problem hiding this comment.
Thanks, the rebase looks right. Everything we asked for is in: no screen chat, no Gemini reader or New chat option, no machine left, and an agent without a reader stays a terminal row. On the rebased stack (main + #180 + #181) we ran tsc, lint, jest and the Python tests, built a Release app and ran npm run e2e in Dark and Light: 52 of 52 (regression/terminal Dark flaked once on "Connected" and passed on a rerun).
#172 is merged (089d3d4), and #174 now targets main. #175 and #176 look good as they are. #174 and this one need a few fixes first (see #174's review for its own), each with a test that would have caught it.
Blocker: the live tail is silent on Debian and Ubuntu hosts (cf1a999, tailScript in src/lib/transcript/store.ts). mawk, the default awk there, buffers what it reads from a pipe, and fflush() flushes only its output. In ubuntu:24.04, tail -c +1 -f f | LC_ALL=C awk '{ print length($0) "\t" $0; fflush() }' printed nothing after a line was appended, not even the first one; with awk -W interactive both lines arrived at once. macOS awk rejects -W, so it has to be chosen per host. The test passed because ShellHost.streamLines drops -f; it should follow the file and append while it reads.
A bridge timeout should reset the connection (2a39e42, LiveBridge.ask in src/lib/herdr/socket.ts). Before, each socket request was an exec, and an exec past its deadline dropped the native client (resetClient()), which is how iOS recovers a half-open socket. The bridge times out on a JS timer only, so on a half-open connection every poll, prompt and Stop waits out its 15 s and fails until TCP gives up, which can take minutes. On a timeout, treat the bridge as dead and let the client reset. Smaller, same file: await live.started isn't bounded by the call's timeout, and after three bridges end unanswered the client stays on exec for good.
Follow-ups from #172, not blocking; a small PR on top is fine:
- A Codex rewind is still missed while the app stays connected.
codexTranscriptPathkeeps the path it found for the connection's life and drops it only when the file disappears, which a rewind's old file never does. - Antigravity: in a step with several calls, an output after the first is tagged
agy-(N+1):0, which no call has, andthreadItemsfalls back only to calls without an id, so the output is dropped. No test has two calls in one step. IntegrationBannershows the raw integration name ("antigravity-cli integration…").codexBase = source.agent === 'codex'inuseThreadgoes against the new CLAUDE.md rule; it belongs onAgentSpec.- The notifier's mtime sort over Codex rollouts can raise if Codex archives one between the search and the sort, and nothing catches it, so the watcher stops.
Heads-up: #182 (Android parity) touches scripts/e2e.sh (a flow list, changed mode, two simulators), HerdrSshModule.kt and the watcher. Whichever lands second will need a rebase; we'll say here.
|
#182 (Android parity) is merged into |
|
One more thing on top of the fixes: please take the five #172 follow-ups from the review above as well, in a small PR on top of the stack once it is rebased onto
Each with a test that would have caught it. Thanks! |
|
Checking in on the stack: nothing has been pushed since the reviews of 20.10, so it stays open as it is.
|
A transcript line with a screenshot is about a megabyte, and the live tail sent it whole: across SSH, through the native bridge, into JSON.parse, for a picture the thread shows as a path. The preview and window reads already strip pictures on the host (#174); the tail could not, because the cursor counts the file's bytes and a stripped line is shorter. The tail now runs through one awk in the C locale, which prints each line's size in the file, a tab, and the line with its pictures stripped. In the C locale `length` counts bytes, so the cursor stays in the file's UTF-8 bytes. The picture pattern is the one the other reads use, spelt out rather than written with `{128}`: older mawk, Debian's awk for years, has no interval expressions, which is why this was held. `fflush` sends each line as it lands. A line without the frame counts as it reads. The store's tests run the real script under sh: a megabyte screenshot line crosses as under 10 KB, and the cursor after emoji, a picture and a data URI matches the file. CI runs it with Ubuntu's mawk. The Demo frames its tail the same way. (cherry picked from commit 79e18b7)
herdr reuses workspace slots. When the host could not be reached, the thread showed the chat's saved messages without checking whose they were (#97), so a slot recycled since the cache was written showed its previous conversation under the new chat's title until the host answered (HC-18). Opened from the list, the thread knows which session the list saw, and the offline seed now reads the cache only when it is that session's (`seedIfSession`, as the instant reopen does). From a link or a notification there is no session to check against, and it reads the cache as before. (cherry picked from commit ff9a8d8)
…used Both guards existed (HC-5): `saveHostKeyPin` stores nothing for an empty fingerprint, and the transport refuses a connection whose host reported none. Neither had a test, and an empty pin reads back as "no pin", which is trust-on-first-use again on every connect. Now both do. (cherry picked from commit db6d082)
An Antigravity chat read its history (07783b8), but its trust, permission and question dialogs had to be answered in the terminal, the `/` palette offered nothing, and its pushes said only the chat's state (HC-109). The dialogs were read off a real agy 1.3.6 through `herdr pane read`, in a scratch workspace, and answered there the way the phone will: - A permission or a question is numbered, and the digit alone answers it: "1" ran the command, "2" chose Red, with no Enter. An Enter after it would land in the composer, so agy is `digitAnswers`. - A long option wraps back to the first column under the menu. A row between two options, no further in than their numbers, is now the label above it going on, so "Yes, and always allow ... for commands that start with 'echo'" shows whole. Claude's descriptions sit further in and are left. - The permission names the tool above its rule and the command under "Requesting permission for:", which now become its details. - The trust question has Claude's heading and answer but `>` for a cursor, and names the agent itself ("Antigravity CLI requires permission ..."). It is read with either cursor, named for the agent the screen names, with the folder joined where a narrow pane wrapped it. The palette offers agy's own 35 commands from the registry in its binary; those that end the conversation are filled, never sent from a tap. Its `ask_question` calls fold as questions. The host watcher quotes agy's replies and names the command it waits on (WATCHER_VERSION 8). (cherry picked from commit c293282)
On iPad the list and the open chat are on screen together, each with its own poll timer. Sharing an in-flight read (efca59d) only helped when the two happened to overlap; on their own schedules the host was asked for every snapshot twice. With no event stream, an idle chat and the list read 50 snapshots a minute where the chat alone reads 30. HerdrClient now remembers the last snapshot it read, whoever asked, and `recentSnapshot(maxAgeMs)` hands it out while it is younger than that. The list's timer takes one younger than its own interval and schedules its next round sooner by that age, so its rows are never older than the interval they were before. Its first round, a round an event or a pull asks for, and every other caller (a send's `currentPane`, the connection check) still ask the host. Alone, as on a phone, the list's own last read is a full interval old when its timer fires, so it asks exactly as before. The budget test now mounts the list beside a chat on one Demo client: 30 snapshots a minute, the same as the chat alone (50 before), and 20 for the list alone. (cherry picked from commit 63833ea)
Two leftovers from the audit's dead-code task (HC-31): - The `previews` table was created on every launch and deleted from on every reset and closed workspace, but nothing ever wrote a row: the list keeps its previews in memory and reads them on the first poll. It is dropped on launch now, as the old `unread` table was, and the deletes go. - Diagnostics carried a `lastError` that was always null, on purpose: no one owns "the last thing that went wrong", and one screen's dismissed error in a bug report misleads. The slot and its line are gone rather than wired to whichever screen failed last. (cherry picked from commit 72519d2)
…e ctx 0 The last three of the audit's small correctness debts (HC-23): - Stop sent Escape and called it done when the host took the key. A host takes a key name it does not know as text, so that proved nothing. Stop now reads the agent's status for up to three seconds and says so when it is still working. It sends one Escape only: a second one into Claude opens its rewind menu. A status it cannot read counts as stopped, so a stop that may well have worked is never reported as failed. - Settings showed one herdr version for the whole app, so after switching hosts it showed the previous host's until the new one's poll landed. The version is now kept per host, and Settings and diagnostics read the selected host's. - A usage block of zeros showed "ctx 0" in the header between turns. Zero is now no label, like a missing count. (cherry picked from commit efffb7a)
Both native modules decrypt a key with a passphrase, and both told a person whose key failed to "check the passphrase", but the app always sent none and had nowhere to type one (HC-7). A key with a passphrase, which is how `ssh-keygen` makes one by default, could not be used at all. - The host editor asks for the passphrase under a private key. It is kept in the keychain like the key, device-only, and cleared with it. A new key brings its own passphrase, blank for none; a kept key keeps its stored one unless another is typed. - NativeChecks now makes an encrypted ed25519 and RSA key with ssh-keygen and proves the iOS code reads each with its passphrase and refuses it without one or with a wrong one. - The guide and the module's README say which keys work where: ed25519 and RSA in OpenSSH format on both, ECDSA on Android only. (cherry picked from commit 0913068)
…uest Every socket request opened an SSH channel and started python3 on the host, so a poll paid that twice a second per open chat and the list's fan-outs queued behind each other on one channel (HC-95). The python bridge now runs once per connection: requests go in on its stdin tagged, answers come back on stdout with the same tag, and each request gets its own socket connection on its own thread, so a waiting agent.prompt holds up no read. Measured against herdr 0.9.5 here: three requests at once, answered out of order, about 17 ms each. The native modules gain writeStream (iOS: SwiftNIO's TTY stdin writer, checked by NativeChecks against a private sshd; Android: the exec channel's output stream). Where a transport cannot write to a command (the Demo) or the bridge cannot start, requests exec as before; three bridges in a row that end without one answer stop the attempts for the client. A timeout is the transport's, since nobody knows if the request landed. The request size limit is kept for both paths. (cherry picked from commit 5e06797)
…ice-locale times A screen reader user heard nothing when the agent stopped to ask, the one moment the app exists for (HC-32). The open chat now announces "<agent> needs you: <question>" once when it blocks and "<agent> replied." when a new reply with text lands; nothing while the history loads, which the screen reader reads anyway. tertiaryLabel is 3.78:1 in light mode, under the 4.5:1 text needs, yet words were set in it: host key fingerprints a person is asked to compare, the working timer, the swipe hint, command hints and section titles, a shell run's time. They are secondaryLabel (6.67:1) now, and a test keeps tertiary to glyphs. Times and dates were en-US with a forced 24-hour clock, whatever the device's region and 12/24-hour setting; they now use the device locale. Of the audit's other HC-32 debts, the splash now matches both backgrounds. (cherry picked from commit 2310851)
On a simulator build without keychain entitlements, the host editor's connection test passed and Save then did nothing at all: the keychain refused the write (-34018), the press handler's promise rejected, and nobody heard. A keychain that refuses a write on a phone, or a database that cannot, would look the same. The editor now stays open and says what failed under the buttons. (cherry picked from commit ec026e9)
A Windows host showed in the chat list as "sukuna: The command failed on the host (exit 1): 'export' is not recognized as an internal or external command,". Windows OpenSSH runs cmd or PowerShell unless told otherwise, neither parses the app's commands, and cmd's words say nothing of the fix. Both shells' "not recognized" now read as `windows_shell`, with the fix the getting-started guide gives: Git for Windows bash as DefaultShell, DefaultShellCommandOption -c. (cherry picked from commit 79a785a)
…ailing On a real host, a new Claude chat in a folder Claude had not been trusted with left New chat open over herdr's own words: "agent probe is blocked during startup and is not ready for prompts". herdr answers agent_not_ready there and keeps the agent registered, waiting at its question, so the workspace and the agent were running while the sheet said it had failed. That answer now counts as started: the chat opens, and its blocked bar asks the question. (cherry picked from commit d52094d)
On a real host, New chat with a mistyped folder made the workspace anyway: herdr creates a workspace for a folder that does not exist in the home folder instead, without a word, and the agent started in ~ with the permissions just chosen for it. The folder is now checked first, on the host, and a missing one is refused with dir_missing before anything is made. The check also reads ~ as the home folder and hands herdr the absolute path. (cherry picked from commit 99bebf8)
…macOS's sh exits 1 CI's Linux runner failed hostFolder.test.ts: its sh is dash, which exits 2 for a cd into a folder that is not there, where macOS's sh exits 1. The client treats any non-zero exit as a missing folder, so the test now asks for that. (cherry picked from commit 22a707b)
Two tests broke only after the rebase onto main: - #186's AgentsBar wrote its "N more" line and its figures in the tertiary label colour, which this branch's contrast test now refuses. They move to secondary; the bar's open/closed chevron is a glyph. - #188's Test connection asks the socket bridge whether it is live, and the save test's mock client had no `socket.detect`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nGz6RQQsBfLJAGByzb1Bn
mawk, the default awk on Debian and Ubuntu, buffers what it reads from a pipe; `fflush()` flushes only its output. Behind `tail -f` the live tail printed nothing on those hosts, not even the first line. mawk's `-Winteractive` reads a line at a time, but macOS awk refuses it, so the script asks `awk -W version` on the host and passes the flag only to mawk. The variable stays a single word, which zsh needs. The tail test dropped `-f`, which is why it passed. A new test runs the script as the host does and appends while it reads. Checked on Ubuntu 24.04 (mawk 1.3.4): 0 of 2 lines before, 2 of 2 after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nGz6RQQsBfLJAGByzb1Bn
Before the bridge, each request was an exec, and an exec past its deadline dropped the native SSH client. That is how the app got off a half-open connection. The bridge timed requests out on a JS timer only, so on a dead link every poll, prompt and Stop waited out its own 15 s, and nothing changed until TCP gave up minutes later. - A request that times out ends its bridge: the channel is closed and what still waits is failed. The next call starts a new bridge, and its start deadline in the native layer drops the client if the link is gone. - Waiting for a bridge to start now counts against the call's own deadline. The start itself carries on, so that native deadline still fires. - After three bridges in a row end unanswered, the host execs for five minutes and then tries a bridge again. Before, it stayed on exec for the life of the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nGz6RQQsBfLJAGByzb1Bn
This branch changes the watcher (Antigravity's dialogs) and moved it from 7 to 8. Meanwhile main went to 8 with a different script. A host that already runs main's 8 would keep it, so the stack's script needs a number of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nGz6RQQsBfLJAGByzb1Bn
60f3e32 to
4ec79a1
Compare
025d543 to
483a64f
Compare
|
Rebased onto the new #176, with the two blockers from the review fixed:
Also, from the rebase:
|
Summary
Rebased onto
mainat 2ce4a9a (#178), with the changes asked for in review. Commit ids below are the rebased ones.The product gaps left after #172 to #176, one commit each: a faster socket path, Antigravity dialogs, encrypted keys, accessibility, and a set of correctness debts from the audit. This branch sits on
hc-107-visual-fixes(#176); merge it after #176.Changes
Speed
2a39e42One socket bridge per connection (HC-95). Every socket request opened an SSH channel and started python3 on the host. The python bridge now runs once per connection: requests go in on its stdin with a tag, and answers come back on stdout with the same tag. Each request gets its own socket connection on its own thread, so a waitingagent.promptholds up no read. Against herdr 0.9.5 on this Mac, three requests at once came back out of order in about 17 ms each.writeStream. On iOS it uses SwiftNIO's TTY stdin writer, which NativeChecks round-trips against a private sshd. On Android it uses the exec channel's output stream (compiled by CI only).timeout), since nobody knows if the request landed. The request size limit holds on both paths.cf1a999Pictures stripped from the live tail on the host (HC-89). The tail istail -c +N -fthroughLC_ALL=C awk, which prints each line's byte count before it, so the cursor stays exact in UTF-8 bytes. The tests run the realshandawk.e2be62eThe list takes the open chat's snapshot on iPad (HC-91): 50 to 30 snapshots a minute with a chat open beside the list.Agents
b9ac497Antigravity dialogs from the phone (HC-109). Measured on agy 1.3.6: a digit alone answers permissions and questions; the trust menu is unnumbered, so Enter or Down then Enter. The parser joins wrapped labels, reads the permission details, and names the agent from "X requires permission to". 35 built-in commands go to the palette. Fixtures are real agy screens with the paths renamed.Correctness and security
cfc2f94Encrypted private keys (HC-7). The host form asks for a passphrase, kept in the keychain beside the key. The docs say which key types work where: ed25519 and RSA on both platforms, ECDSA on Android only.ae566b8Small debts (HC-23). Stop now checks that the agent leftworkingand says so if it did not (interrupt_unconfirmed), sending Escape once. The host's herdr version is kept per connection. A context of 0 shows nothing.e661f62Offline, no recycled slot's chat (HC-18). Opened from the list offline, a chat shows its saved messages only when the list saw the same session.a1a601fTests for the host-key pin (HC-5): an empty key is never pinned, and a nameless key is refused.141215cDead code (HC-31): thepreviewstable nothing wrote (dropped) and the diagnostics slot nothing filled.149eed0Save says why it failed (HC-119). On a simulator build without keychain entitlements, the connection test passed and Save then did nothing: the keychain refused the write and the press handler's rejection went nowhere. The editor now stays open and says what failed.1e0216eA Windows host is told to use bash (HC-118). A Windows computer reached over SSH showed cmd's own "'export' is not recognized as an internal or external command," in the list. cmd's and PowerShell's "not recognized" now read aswindows_shell, with the fix the guide gives: Git for Windows bash as the OpenSSHDefaultShell.6a34191A new chat whose agent stops at a startup question opens (HC-129). A new chat in a folder the agent had not trusted failed in New chat with herdr's own words, "blocked during startup", while the agent kept waiting. herdr answersagent_not_readyand keeps the agent; that now counts as started, and the chat's blocked bar asks the question.c18c12bA new chat's folder is checked first (HC-130). herdr makes a workspace for a folder that does not exist in the home folder instead, without a word: a mistyped path started an agent in~on a real host. The folder is now checked on the host (and~read as the home folder) before anything is made, and a missing one is refused withdir_missing.Accessibility (HC-32)
704a431VoiceOver now hears " needs you: " when the open chat's agent stops to ask, and " replied." when a new reply lands. Text that was intertiaryLabel(3.78:1 in light mode) moves tosecondaryLabel(6.67:1). That covers host key fingerprints, the working timer, the swipe hint and the command hints, and a test keeps tertiary to glyphs. Times and dates follow the device's locale and 12/24-hour setting instead of en-US.Dropped in review
wB,regression/gemini, and the message upsert, which only Gemini's log needed.Tests
socketBridge.test.ts: a fake bridge (answers out of order, a batch entry by entry, a timeout, a bridge that ends, three silent bridges, a failed start), and the real python bridge undershagainst a Unix socket that answers like herdr (a slow request holds up no other; an emoji survives; a missing socket says herdr is not running).ServerEditScreen.test.tsx(a failed keychain write) andhostErrors.test.ts(three real forms of the Windows message) fail without their fixes.sendPrompt.test.ts(agent_not_ready),hostFolder.test.ts(the folder check under a realsh),clientSocket.test.ts(nothing made for a missing folder) fail without their fixes.announce.test.ts,formatListTime.test.ts,tertiaryText.test.tsfor HC-32.Test plan
npx tsc --noEmit: no errorsnpx expo lint: no errors, no warningsnpx jest: 2,064 passed, 14 skippedpython3 -m unittest discover -s scripts -p 'test_*.py': passesswift run --package-path modules/herdr-ssh NativeChecksagainst a private sshd: passes, the stdin round-trip includednpm run e2ein Dark and Light on the iPhone 17 Pro simulator (iOS 27), Release build of the rebased stack (Make every agent herdr detects visible: Grok and Antigravity chats, terminal rows for the rest, Codex fixes #172 to Close the remaining product gaps: one socket bridge, Antigravity dialogs, encrypted keys, accessibility #177): 52 of 52, the four flows5a4cf24fixes rerun on their own after it. Screenshots opened in both modes.writeStream: the CIandroidjob passes on this branch now thatmain(with Make dev builds open again, build native code in CI, run the UI suite nightly #173) is merged in.antigravity-cliintegration: New chat opened the chat at agy's trust question, the answer from the app went through, the session was reported, the history and model showed, a permission dialog (pwd) was answered from the app, and the/palette listed agy's commands. The integration was uninstalled after, and agy's config checked against a snapshot.