Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ jobs:
- run: cargo fmt --check
- run: cargo clippy --all-targets -- -D warnings
- run: cargo test --lib
- run: cargo test --test sftp_prompt
135 changes: 133 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ publish = false
anyhow = "1.0.104"
directories = "6.0.0"
eframe = "0.36"
egui_extras = { version = "0.36", default-features = false, features = ["syntect"] }
egui_extras = { version = "0.36", default-features = false, features = ["syntect", "svg"] }
egui_dock = { version = "0.21.1", features = ["serde"] }
futures = "0.3"
keyring = { version = "3", features = ["windows-native"] }
Expand All @@ -31,6 +31,9 @@ tokio = { version = "1.53.3", features = ["full"] }
ureq = { version = "2.12", default-features = false, features = ["tls", "json"] }
uuid = { version = "1.28.0", features = ["v4", "serde"] }
vt100 = "0.16.2"
vte = "0.15"
arboard = "3.7"
tempfile = "3"
zeroize = "1"
portable-pty = "0.9"
serde_norway = "0.9.42"
Expand All @@ -41,7 +44,6 @@ notify-rust = "4"
toml_edit = "0.25"

[dev-dependencies]
tempfile = "3"
egui_kittest = { version = "0.36", features = ["eframe", "wgpu", "snapshot"] }

[profile.release]
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ A personal project: the split-pane workspace of Termius plus the SFTP speed of B
- **Secure by default**: secrets live in the OS credential store, host keys are trusted on first use, and tether refuses to connect if a host key changes.
- **Customisable**: themes, and every shortcut can be rebound (*Settings → Keyboard*).

### Terminal and agent shortcuts

- **Shift+Enter** inserts a line without submitting in Claude Code, Codex and integrated PowerShell, Bash, Zsh and Fish prompts. Enter submits normally.
- **Images**: a local terminal running a detected Claude Code or Codex session shows **+ Image** and accepts screenshot / PNG / JPEG attachments. Ordinary shells use the full terminal height and do not show the image toolbar. Colored **[Image 1]** labels show a thumbnail on hover; Tether inserts a usable file path for the agent. Local temporary copies are removed when the terminal closes. **Clear previews** only hides the thumbnails; edit the prompt in the terminal to remove an inserted path.
- **Claude Code / Codex**: Codex has its own movable sidebar view with terminal sessions, activity, attention badges, CPU / memory, last messages and navigation, using the same monitoring system as Claude. Configure Claude hooks and Codex completion notifications in **Settings → AI agents**. Codex `notify` reports completed turns; ongoing work is tracked from terminal activity and processes.
- **Custom Boot**: right-click either agent icon, enter your local command and choose its shell. Tether passes the command and flags through unchanged, including account selectors implemented by your own functions or aliases. Choose the shell whose startup profile defines them (Windows PowerShell and PowerShell 7 use different profiles). The shell is remembered separately for each agent.
- **Project launchers**: both normal launch and Custom Boot ask for a local project directory in the same card. Type a path (including `~`), use Tab / Shift+Tab completion or browse. Give it a name and choose **☆ Save** to pin a shortcut shared by Claude and Codex. Local terminal folders are remembered automatically, with up to 32 recent folders; right-click a shortcut to pin, unpin or forget it.
- **Terminal colors**: local PTYs and SSH sessions share `xterm-256color` / true-color capability hints. Local shells do not inherit a GUI runner's `NO_COLOR`; ANSI, indexed and RGB colors use the existing terminal renderer and theme palette. SSH servers may restrict optional environment hints through `AcceptEnv`; their PTY still advertises 256 colors.
- **Remote path links**: click a remote path to open SFTP beside that terminal, using the same companion pane as **Ctrl+Shift+F**; right-click also offers **Open in SFTP**, the editor and copy options. Repeated clicks navigate the existing pane. Paths inside prompts such as `prisma@host:~/project$` retain the prompt's account: `~` resolves to prisma's actual home on the server, even when SSH logged in as root and the shell used `su` / `sudo`. No `/home/<user>` assumption is made. Relative paths resolve against the source terminal's directory; files open their containing folder.
- **Tab close targets**: hovering the close button or its surrounding area keeps the tab's current expansion, so resource details do not move the X while you aim for it.

### Share machine setups and find SSH keys

Open **Import / export setup…** from the app or Hosts menu. **Export setup** previews the machines and explains that anyone with the JSON file can import their names, IP addresses / hostnames, ports, usernames and colors. Keys, key paths, passwords and passphrases are excluded. Choose the **Tether setup** source card under **Import**, review the selected machines and import them; existing endpoints are skipped and imported machines need credentials before connecting.

**Search this PC** in Import finds Bitvise profiles in common OS-provided folders. **Search this PC for SSH keys…** in the key manager or host editor searches `.ssh`, Bitvise locations, Documents, Desktop and Downloads, and displays each result's full path. Searches run in the background with a depth/time/file limit and skip symbolic links. You choose what to import; **Browse…** handles other locations. Export keys held in Bitvise's encrypted internal store as OpenSSH files from Bitvise before importing them.

## Install

Download `Tether-Setup-<version>.exe` from [Releases](https://github.com/izLoki/Tether/releases) and run it. No admin rights needed. It adds Start menu and desktop shortcuts. Tether checks for new releases on startup and installs them in one click (*Settings → About → Updates*). Running a newer installer also upgrades in place.
Expand Down
1 change: 1 addition & 0 deletions assets/agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Codex mark is the OpenAI blossom used by the official Codex desktop app. Source: Codex for Windows 26.1013.4894.0, webview/assets/logo-18892d25d393.svg. The geometry is unchanged; the fill is white so the UI can tint it for light and dark themes. OpenAI retains its rights to the mark, which identifies the integrated product. Brand guidelines: https://openai.com/brand/
1 change: 1 addition & 0 deletions assets/agents/codex.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 9 additions & 4 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ pub enum Command {
OpenLayoutSettings,
/// The "Import hosts" dialog (Bitvise, SSH config).
ImportHosts,
/// Start an AI agent CLI in a new default shell.
LaunchAgent { kind: crate::agents::AgentKind, command: &'static str, pick_folder: bool },
/// Choose a local project and shell, then start an AI agent CLI.
LaunchAgent {
kind: crate::agents::AgentKind,
command: &'static str,
},
/// Anything else: runs on the UI thread when chosen.
Run(Arc<dyn Fn(&egui::Context) + Send + Sync>),
}
Expand Down Expand Up @@ -207,7 +210,8 @@ fn action_keywords(a: Action) -> &'static str {
Action::NewLocalTerminal => "shell console",
Action::ToggleBroadcast => "all terminals sync",
Action::NextAttention => "agent waiting jump",
Action::ToggleAgents => "ai claude codex",
Action::ToggleAgents => "ai claude",
Action::ToggleCodex => "ai codex openai",
_ => "",
}
}
Expand Down Expand Up @@ -241,7 +245,8 @@ pub fn fuzzy_match(needle: &str, hay: &str) -> Option<(i32, Vec<usize>)> {
let lower_str: String = lower.iter().collect();
let substring_at = lower_str.find(&needle_str).map(|byte| lower_str[..byte].chars().count());

let word_start = |i: usize| i == 0 || matches!(h[i - 1], ' ' | '-' | '_' | '/' | '\\' | '.' | ':' | '·' | '(') || (h[i - 1].is_lowercase() && h[i].is_uppercase());
let word_start =
|i: usize| i == 0 || matches!(h[i - 1], ' ' | '-' | '_' | '/' | '\\' | '.' | ':' | '·' | '(') || (h[i - 1].is_lowercase() && h[i].is_uppercase());

// Greedy forward pass, preferring word starts: for each needle char take the next
// word-start occurrence if one comes before the next plain occurrence's run ends.
Expand Down
Loading