Files
dial/dial-v2-plan.md
bsncubed 8af44dfcd2 Initial commit: dial — interactive SSH host picker
A fast, read-only ~/.ssh/config picker that exec's the system ssh client.
Includes: static high-contrast picker with recency/frequency/name sort and
tag grouping, `dial keygen` (native ed25519/RSA + optional config entry), and
an optional offline YubiKey launch gate with one-time recovery codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:18:23 +10:00

11 KiB

dial — v2 Plan

Written against the v1 REPORT.md handoff. v1 is complete, tested, and out of scope to modify except where noted. This plan is scoped to be handed to Claude Code as-is; every judgment call is flagged so it can be overridden.


1. Alphabetical sort (small, do first)

  • Add a third sort mode to the existing s toggle: recently used → most used → alphabetical → (back to recently used).
  • Alphabetical sorts by Host alias (the thing the user actually types/sees first in the list), case-insensitive.
  • Favourites still pin to the top regardless of sort mode (unchanged v1 behaviour).
  • Persist the selected mode the same way recents/frequency already persist (~/.ssh/.dial/data.json).

No open questions here — this is a straightforward extension of existing code.


2. SSH key generation tool (dial keygen)

Rebuilt from the logic in create_ssh_keypair.sh, fixing the cross-platform and UX issues found in it, and wired into dial as a new subcommand rather than folded into the interactive picker.

Why a separate subcommand, not part of the picker: the picker's core read-only-config guarantee is a deliberate v1 property. Keygen necessarily writes to ~/.ssh/config and creates key files, so it should be a distinct, explicitly-invoked action (dial keygen) — not something that can happen by accidentally pressing a key while browsing hosts.

Fixes over the original script

  • Replace the macOS-only sed -i '' public-key-comment logic with something that works identically on macOS/Linux/Windows (Go string handling, no shell sed dependency at all).
  • Default algorithm: ed25519. Offer --rsa (4096-bit) as an explicit opt-out for hosts that don't support ed25519 (some older network gear, e.g. legacy broadcast equipment, may not).
  • Keep the .pub file. Don't delete it after displaying it once — that behaviour in the original script means a lost copy-paste = a wasted key. Still print it to the terminal for convenience, but leave the file in place.
  • Proper quoting/escaping when appending to ~/.ssh/config (avoid the blind read + string-concat approach in the original).

Proposed flow

dial keygen
  → prompt: key name / label
  → prompt: algorithm (default ed25519, flag for rsa)
  → generate into ~/.ssh/<label>  (and .pub alongside, kept)
  → prompt: add a Host entry to ~/.ssh/config now? (y/n)
      → if yes: Host alias / HostName / User / Port (default 22) / tag (optional, using existing `# tag:` convention)
  → print the public key once for easy copy, key file left on disk

Assumption flagged: this reuses dial's existing static-list, low-light- friendly prompt styling rather than plain unstyled read prompts like the original script, for consistency with the rest of the tool.


3. Remaining v2 features (from REPORT.md open questions)

Going through the report's nine open questions individually, with a decision made and flagged for each so this can go straight to Claude Code.

3.1 SFTP / file transfer mode — in scope for v2: full in-app browser

  • After selecting a host, a second action (t for transfer, alongside enter to shell in) opens an in-app SFTP file browser — not a shell-out to the system sftp binary.
  • Built directly against a Go SFTP library (pkg/sftp + golang.org/x/crypto/ ssh for the underlying connection) rather than shelling out, so dial owns the whole UI.
  • Core browser UX (following the same static, high-contrast, low-light- friendly list styling as the host picker — no cramped/dense text):
    • Split view or toggle-view of local vs. remote directory listing.
    • Arrow-key navigation through directories on both sides; enter on a directory descends into it, a clear "up one level" entry at the top of each listing.
    • Batch selection: a keypress (e.g. space) toggles selection on the highlighted entry without moving the cursor, same pattern as v1's f favourite toggle. Multiple files (and/or directories) can be selected before triggering a push/pull.
    • Push (local → remote) or pull (remote → local) on the current selection — a single file, multiple files, a single directory, or a mix.
    • Directory transfers are recursive, walking the full tree on the source side and recreating structure on the destination side.
    • Progress UI: per-item progress plus an aggregate "3 of 12 files, 40% of total" view when a batch/directory transfer is in flight — legible at the same size/contrast as the rest of the UI.
    • Clear error states: connection drop mid-transfer, permission denied, disk full, file already exists (prompt overwrite/skip/rename rather than silently clobbering) — applies per-item within a batch, with an option to apply the same choice to all remaining conflicts rather than prompting per file.
  • Resume on failure: basic, same-session only. If a transfer (or a batch/ directory transfer) is interrupted — network drop, connection reset — and retried within the same dial session, dial seeks both the local and remote file to the last confirmed byte offset and continues rather than restarting from zero. This applies per-file within a batch/directory transfer (finished files stay finished; only the interrupted one resumes).
    • Explicitly not included: resume that survives closing/restarting dial or the machine. That requires persisting transfer state to disk and verifying partial files weren't corrupted before trusting them — a meaningfully bigger, separate piece of work. Flag if this turns out to matter in practice (e.g. regularly moving very large files where a lost session is costly) and it can be scoped as a v2.x follow-up.
  • Reuses the connection details already resolved from ~/.ssh/config for the selected host (same auth, same ProxyJump if present) — no separate credential entry.
  • Gate coverage: as already decided in §3.4, the YubiKey gate (when enabled) covers entry into this mode too, not just app launch.

3.2 How to pick the action (shell vs SFTP) — decided

  • Second keypress on the highlighted host, not a separate menu: enter = ssh, t = sftp. Keeps the static-list simplicity intact rather than adding a submenu/modal.

3.3 Distribution/trust (signing, Homebrew, Scoop, CI) — deferred, not v2

  • This is packaging/ops work, not a feature, and doesn't block daily use on your own machines via Syncthing + manual install. Flagging as a separate future task rather than bundling into this feature-focused v2 pass.

3.4 Gate ergonomics — small change: gate now also covers SFTP

  • Every-launch touch stays as-is (no idle-timeout added — the report notes this as a "worth adding later" maybe, not a v2 must-have). Assumption flagged: leaving idle-timeout out of v2; say the word if you want it added now instead.
  • The YubiKey gate (when enabled) now also gates the new t (SFTP) action, not just app launch, since it's a second way to move data off/onto a host.

3.5 Recovery-code UX — unchanged

  • Current design (terminal-only, shown once, single-use, auto-rotates) stays as-is. No changes requested and the report doesn't flag it as a problem.

3.6 Richer metadata (tag grouping/colour-coding) — in scope, small

  • Add optional section headers by tag in the static list when sorted alphabetically or by tag (new sort mode candidate — see note below).
  • Colour-code tag pills using the existing internal/theme high-contrast palette (e.g. a distinct colour per tag), not free-form colour — keeps the low-light-readable requirement intact.
  • Open question for you, not decided here: should "group by tag" be its own 4th+ sort mode, or a toggle independent of sort? Recommend: independent toggle (g to group), since it's orthogonal to alphabetical/recent/most-used ordering. Flagging for Claude Code to confirm with you if ambiguous during build.

3.7 Search behaviour (fuzzy vs substring) — kept as substring, not changed

  • Report explicitly chose substring to honour the "readable, not cramped" ethos. No reason surfaced in this conversation to revisit — leaving as-is for v2.

3.8 Multi-select / batch actions across hosts — deferred, flagged as scope creep

  • Note: this is distinct from the batch file selection now included in the SFTP browser (§3.1), which is about selecting multiple files/folders within one host's transfer session. This item is about running a command or transfer across multiple hosts at once — the report itself raises this as possible scope creep. Recommend leaving it out of v2 — that's a meaningfully different tool shape (more like an orchestration tool than a picker) and deserves its own dedicated design pass if wanted later, not a bolt-on here.

3.9 Connection history/analytics beyond recents — small addition

  • Add a last-connected timestamp shown in the preview pane (already tracking recency internally for sort — this just surfaces it visually).
  • Per-host notes: deferred — no clear use case surfaced yet; easy to add later without restructuring anything, so not worth the effort now.

v2 Scope Summary

Building now:

  1. Alphabetical sort mode
  2. dial keygen subcommand (cross-platform, ed25519-default, config-append)
  3. In-app SFTP file browser (t to open, pkg/sftp-based — local/remote listings, batch file/folder selection, recursive directory transfers, same-session resume-on-failure, aggregate progress, overwrite prompts)
  4. Tag grouping toggle + colour-coded tag pills
  5. YubiKey gate extended to cover the new SFTP browser
  6. Last-connected timestamp in preview pane

Explicitly deferred (not this pass):

  • Resume that survives closing/restarting dial (crash-persistent resume)
  • Idle-timeout gate option
  • Distribution/signing/CI/package managers
  • Multi-select/batch actions across hosts (§3.8) — distinct from in-browser batch file selection, which is now in scope
  • Per-host notes
  • Fuzzy search

Notes for Claude Code

  • The SFTP browser is a genuinely new subsystem (internal/sftp or similar), not a reuse of internal/connect's exec-handover pattern — it needs its own connection lifecycle via pkg/sftp/golang.org/x/crypto/ssh since dial stays in the foreground driving the UI, rather than handing off the process like the SSH connect flow does.
  • dial keygen is a new top-level subcommand alongside the existing yubikey enable/disable/status/recovery, version, help — not part of the TUI picker's keybindings.
  • Confirm with the user during implementation: tag-grouping as toggle vs sort mode (§3.6), if it's not obvious once in the code.
  • Given the SFTP browser is now the largest single piece of v2, worth sequencing it last (after alphabetical sort, keygen, tag grouping, last- connected timestamp) so the smaller wins land first and the browser gets full attention/testing time.