Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(cli): mir opens your machines — the live overview (O1) - #97

Merged
frahlg merged 1 commit into
mainfrom
94-mir-overview
Aug 30, 2026
Merged

feat(cli): mir opens your machines — the live overview (O1)#97
frahlg merged 1 commit into
mainfrom
94-mir-overview

Conversation

@frahlg

@frahlg frahlg commented Aug 30, 2026

Copy link
Copy Markdown
Member

Implements spec D3 (docs/superpowers/specs/2026-08-30-invisible-tmux-one-path-design.md). Closes #94.

What it looks like

 mir — your machines

 ▸ ● zap-dev  NEW
     3 windows — claude, build, logs
   ○ office-mini

 enter attach · r rename · x retire · q quit · ? help

Enter attaches the selected machine; Ctrl-O then d comes back. r renames inline (N1's core), x retires after a one-line y/N with N2's facts. The list refreshes every 3 s through the T2 caches (most ticks cost nothing) and re-renders only on change. The cursor starts on the last-used machine.

Decisions

  • One live attach at a time (v1). The overview process is the warm place — the T2 caches are primed on entry (TURN included, so Enter is warm) — but a bounded multi-warm session pool like the SPA's is deliberately deferred; the overview redraws on return instead. Noted for a follow-up.
  • Detach gesture: Ctrl-O then d (or q), the mux's prefix convention. A local stdin filter watches for it and cancels the attach context BEFORE reporting EOF, so the R1 reconnect loop sees a cancelled context — not a dropped link to redial. Prefix twice passes a literal Ctrl-O through; prefix+other passes both bytes.
  • stdin is pumped once. One goroutine owns os.Stdin for the whole overview session; the overview and each attach consume the same channel, so the keyboard hands back and forth without re-opening stdin.
  • Bare mir attach continues: last-used machine (persisted on each successful attach), else the only machine, else the overview. TTY-only — a script's bare mir attach keeps today's usage error, and non-TTY mir keeps the static guide + exit 2.
  • mir --help / -h / help now print the guide (exit 0); they previously fell through to the terse usage + exit 2.
  • Fresh setup and legacy identities land on the guide, not on a keychain prompt or an unusable screen.
  • NEW badge pins the machines the seen-set didn't know when the overview opened, holds for the whole run, and records them so the next run starts clean.
  • FrameWindows is consumed through a new optional WindowsSink on the bridge — the byte stream is untouched, and the summary is one dim line, not a window strip.
  • No TUI framework: raw mode + ANSI on golang.org/x/term, same as the mux. A menu, not a multiplexer.

Tests

gofmt -l empty, go vet clean, go test ./... green, -race green on cli+client, web 152/152 (untouched). New coverage: key decoding incl. split escape sequences, rendering (rows/badges/empty state/prompt), cursor clamping, the windows summary, pickDefaultMachine, the detach filter (pass-through, doubled prefix, pre-gesture bytes, split gesture), and the last-used round trip.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y


Note

Medium Risk
Large new interactive CLI path (raw terminal, attach lifecycle, shared revoke/rename over the network) changes default TTY entry behavior; script/non-TTY paths are preserved but reviewers should sanity-check attach/detach and revocation flows from the overview.

Overview
Adds a TTY-first “your machines” overview when you run bare mir (or when bare mir attach has no obvious single target). It’s a raw-mode picker—online/offline dots, optional tmux window summary lines, periodic registry refresh via existing prewarm caches, inline rename/retire, and Enter to attach. Ctrl-O then d/q returns to the overview via a stdin pump + detach filter that cancels attach before EOF so reconnect doesn’t fire.

Bare mir attach on a terminal now means “continue”: last-attached machine (new lastused.json), else the only paired machine, else the overview. Scripts/non-TTY behavior is unchanged (usage error / static guide + exit 2). --help / -h / help print the guide and exit 0.

Shared machine ops: rename and revoke logic move into renameResolved / retireResolved for both mir machine … and the overview. Successful attaches persist last-used. The client bridge gains an optional WindowsSink so overview can show tmux summaries without altering terminal output. UnseenMachines supports NEW badges for the overview session.

Reviewed by Cursor Bugbot for commit 5f69ca0. Bugbot is set up for automated code reviews on this repo. Configure here.

`mir` with no arguments on a terminal now opens a live picker: one row per
machine (online/offline dot, NEW badge, dim tmux window summary), arrows or
j/k to move, Enter attaches, r renames inline, x retires with the N2 facts,
q quits. Ctrl-O then d detaches back to the overview. Bare `mir attach`
continues where you left off (last-used, else the only machine, else the
overview). The CLI now consumes the FrameWindows snapshot the agent already
pushes. Scripts see no change: non-TTY `mir` keeps the static guide + exit 2,
and bare `mir attach` without a TTY keeps its usage error.

Built on raw terminal + ANSI only — a menu, not a multiplexer; tmux keeps
owning windows, the mux keeps owning machine focus.

Closes #94

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T09:20:37.566185Z 5f69ca0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f69ca078f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +164 to +168
turnURL := ""
if first {
turnURL = defaults.SignalURL()
}
warm := ov.app.prewarm(ctx, ov.dir, ov.idn, turnURL, nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fetch TURN credentials from the selected relay

When the selected machine's SignalURL differs from defaults.SignalURL(), this prewarm stores TURN credentials minted by the default relay and attach later passes them to client.Attach through ov.ice(). Prewarm.ICEFrom explicitly documents that credentials must not be reused for another relay, so overview attaches that require TURN behind strict NATs will fail for custom/federated-relay machines; resolve or cache ICE using the selected machine's relay before dialing.

Useful? React with 👍 / 👎.

Comment on lines +601 to +602
if name == "" {
return a.cmdOverview()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the selected state directory into the overview

When mir attach --dir <custom> has multiple machines and no valid last-used target, this fallback calls cmdOverview(), which unconditionally loads defaultClientDir() rather than <custom>. The command therefore switches identities and displays the default machine list after having resolved the decision from the custom directory; make the overview accept and retain the parsed directory.

Useful? React with 👍 / 👎.

Comment on lines +400 to +402
func(ctx context.Context, mc peer.MsgConn, sess *noise.Session) error {
once.Do(func() { client.SaveLastUsed(ov.dir, m.Name) })
return runBridgePumped(ctx, mc, sess, in, sink)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop the prior stdin reader before redialing

After a transport drop, ClientBridgeSink can return from its receive goroutine while its stdin goroutine remains blocked in detachFilter.Read, because canceling the bridge context does not unblock that read. ReconnectLoopWith then invokes this callback again with the same in, leaving multiple goroutines competing for the pump; the stale reader can consume the first keystrokes after reconnection and attempt to send them on the closed connection. Use one persistent input forwarder or make each bridge read cancelable and wait for it to exit before redialing.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f69ca0. Configure here.

warm := ov.app.prewarm(ctx, ov.dir, ov.idn, turnURL, nil)
if first && warm.ICEErr == nil && len(warm.ICE) > 0 {
ov.iceList = warm.ICE
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TURN credentials freeze after first refresh

Medium Severity

refresh requests TURN only on the first tick and stores the result in ov.iceList for the rest of the run. Later ticks pass an empty turnURL, so the T2 ICE cache is never consulted again. Relay credentials expire after 15 minutes, and a failed first fetch is never retried, so Enter from a long-lived overview can attach with dead or missing TURN.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f69ca0. Configure here.

sink := func(payload []byte) {
if line := windowsSummary(payload); line != "" {
ov.windows[m.Name] = line
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows map written without synchronization

Medium Severity

The WindowsSink writes ov.windows from the bridge receive goroutine, while the main loop reads that map after attach returns and again in refresh. ClientBridgeSink does not wait for the receive goroutine to exit, so those accesses overlap. Concurrent map read/write can panic.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f69ca0. Configure here.

func(ctx context.Context, mc peer.MsgConn, sess *noise.Session) error {
once.Do(func() { client.SaveLastUsed(ov.dir, m.Name) })
return runBridgePumped(ctx, mc, sess, in, sink)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconnect races the shared detach filter

Medium Severity

One detachFilter is reused for every ReconnectLoopWith session. When a drop ends the bridge via the receive side, the previous stdin goroutine stays blocked in detachFilter.Read. The next session starts a second Read on the same filter. Keystrokes after a reconnect can be stolen by the dead session, and a split Ctrl-O d can miss the detach.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f69ca0. Configure here.

ov.windows[newName] = ov.windows[m.Name]
}
ov.refresh(ctx, false)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename leaves last-used name stale

Low Severity

Last-used is stored by display name and is not updated when that machine is renamed. pickDefaultMachine and the overview cursor both look up LastUsed by name, so after a rename the continue path no longer finds the machine the user was on.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f69ca0. Configure here.

return err
}
if name == "" {
return a.cmdOverview()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview ignores attach directory flag

Low Severity

When a bare mir attach --dir … has no default target, it calls cmdOverview, which always uses defaultClientDir(). Discovery used --dir, but the picker then shows a different state directory.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f69ca0. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved: Cursor Bugbot finished as skipped and reported five unresolved issues that need human review. Reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor
cursor Bot requested review from davmoz and wachtelhund August 30, 2026 09:24
@frahlg
frahlg merged commit d99af10 into main Aug 30, 2026
5 checks passed
frahlg added a commit that referenced this pull request Aug 30, 2026
* assets: demo GIF for the reach-layer flow

The old demo told a story the software no longer tells: a `LAN-direct on
(mDNS + QUIC)` line for a transport deleted in #92, `wallet …` for what
v0.7 renamed to identity, and a `mir up → mir list → mir attach` flow that
predates the overview (#97), the first-run pairing QR, the tmux-style
aliases, and session sharing (#101#104).

The new take is the current one, in four beats: bare `mir` opens the live
overview; Enter attaches and the tmux session with a long-running agent is
right there; Ctrl-O d comes back, and the row now remembers what runs over
there; then one invite, read-only, gone in an hour.

Every line the script prints is a line `mir` prints. The overview screen is
overviewModel.Render()'s output, the attach banner is overview.go's, and
the whole `mir share` block — header, QR, both invite lines, the wait line
— was captured verbatim from a real run against relay.sourceful-labs.net.
The QR in the rendered GIF decodes to the join URL printed beside it. Only
the tmux screen and the shell prompt are authored, and neither is mir's
output.

The canvas grew to 1100x880 (43 rows x 102 cols) so the invite QR fits on
one screen without scrolling. 219 KB, 12.8 s.

Part of #106

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y

* assets: narrow the demo canvas so every cell reads bigger

1100x880 at 102 columns left the 📱 in the mint header about 8 px wide once
GitHub scaled the GIF into its 900px column, small enough to read as a box.
No font fixes that: `fc-list ':charset=1F4F1'` finds exactly one face on this
machine, Apple Color Emoji, and a 10x crop shows that is already the glyph
being drawn — bezel, dark screen, colored icons. It is Apple's black phone on
a dark theme, not a tofu box. Naming a font in the tape only makes it worse:
every installed monospace face falls back to the same Apple glyph, and a
comma list starting with JetBrains Mono breaks the cell metrics outright,
because vhs supplies that face as a webfont and the list form defeats it.

So the lever is columns, not fonts. 884x878 gives 42 rows x 80 columns: the
last beat needs 41 rows, and 80 is the narrowest width that still wraps each
invite URL to two lines rather than three. Every cell — the emoji included —
grows about a quarter, and the frame stops being mostly empty.

Widths must stay even. 883 defeated the palette pass and turned the same
recording into 14 MB; 884 renders it at 214 KB.

214 KB, 12.8 s, 884x878. The QR still decodes to the join URL printed
beside it.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y

---------

Co-authored-by: Claude Fable 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

O1: mir opens your machines — the live overview

1 participant