Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c22dcc1
feat(desktop): add switchyard-desktop --print for recorded costs and …
elyasmnvidian Oct 23, 2026
8c30afb
feat(desktop): add the terminal app shell and Overview server status
elyasmnvidian Oct 23, 2026
ac7f5b0
feat(desktop): add Overview usage cards, the hourly chart, and the he…
elyasmnvidian Oct 23, 2026
675f1f7
feat(desktop): add model share tables and the Models page to the term…
elyasmnvidian Oct 23, 2026
302d9b8
feat(desktop): add the Calls page and live call filter to the termina…
elyasmnvidian Oct 23, 2026
9564b6d
feat(desktop): add the Settings page, server actions, and editor handoff
elyasmnvidian Oct 23, 2026
b705736
feat(desktop): add Settings forms, source update review, and action c…
elyasmnvidian Oct 23, 2026
a223134
chore(desktop): organize terminal actions by page and add review popups
elyasmnvidian Oct 23, 2026
b779e7b
feat(desktop): parse server routes and add the Routes table to the te…
elyasmnvidian Oct 23, 2026
f4bff34
feat(desktop): show route details and endpoint status in the terminal…
elyasmnvidian Oct 23, 2026
81d7838
feat(desktop): fetch and cache upstream model lists from the terminal…
elyasmnvidian Oct 23, 2026
b28bbd7
test(desktop): cover model-list key scoping, curl reaping, and error …
elyasmnvidian Oct 23, 2026
39b845e
feat(desktop): preview route target and algorithm edits in server con…
elyasmnvidian Oct 23, 2026
15eed30
feat(desktop): edit route algorithms and models in the terminal app
elyasmnvidian Oct 23, 2026
4d188e9
feat(desktop): create, rename, duplicate, and delete routes in the te…
elyasmnvidian Oct 23, 2026
15a94df
test(desktop): verify route editing preserves shared targets and backups
elyasmnvidian Oct 23, 2026
9d38181
test(desktop): verify route review tokens, staleness, and terminal ro…
elyasmnvidian Oct 23, 2026
33c493e
feat(desktop): inspect harness settings and add the Harnesses page
elyasmnvidian Oct 23, 2026
ba840e9
feat(desktop): preview harness settings for Codex, Claude Code, and Pi
elyasmnvidian Oct 23, 2026
683a717
feat(desktop): install and restore reviewed harness settings
elyasmnvidian Oct 23, 2026
71aac00
feat(desktop): add named harness accounts to the Harnesses page
elyasmnvidian Oct 23, 2026
aac8ca9
test(desktop): verify harness preview, install, and restore edge cases
elyasmnvidian Oct 23, 2026
8641778
test(desktop): cover named accounts and the Windows terminal launcher
elyasmnvidian Oct 23, 2026
a84d509
feat(desktop): add the Sessions page and recorded session inventory
elyasmnvidian Oct 23, 2026
4ec7571
feat(desktop): launch isolated worktree sessions from the terminal app
elyasmnvidian Oct 23, 2026
366bc76
feat(desktop): open and clean up worktree sessions in the terminal app
elyasmnvidian Oct 23, 2026
f44fbb9
test(desktop): verify session launch reviews, cleanup, and account is…
elyasmnvidian Oct 23, 2026
a335cc0
feat(desktop): record session launches and open harnesses in existing…
eric-liu-nvidia Oct 26, 2026
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
3 changes: 2 additions & 1 deletion crates/switchyard-desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[package]
name = "switchyard-desktop"
version.workspace = true
description = "Installer and desktop app for a local Switchyard server"
description = "Installer, desktop app, terminal app, and usage summary for a local Switchyard server"
authors.workspace = true
edition.workspace = true
homepage = "https://github.com/NVIDIA-NeMo/Switchyard"
Expand All @@ -26,6 +26,7 @@ toml = "0.9"
toml_edit = "0.25"
url = "2"
uuid.workspace = true
# Popups use Paragraph::line_count to size and scroll wrapped text.
ratatui = { version = "0.30.2", features = ["unstable-rendered-line-info"] }
shlex = "2"
strum = { version = "0.28", features = ["derive"] }
Expand Down
232 changes: 232 additions & 0 deletions crates/switchyard-desktop/src/accounts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Named login directories use the harness's own authentication commands.

use crate::harness::{self, Harness};
use std::path::{Path, PathBuf};

// PendingAccount removes only an empty directory if login setup fails.
// The harness may already have saved login files, which must stay.
struct PendingAccount(Option<PathBuf>);

impl Drop for PendingAccount {
fn drop(&mut self) {
if let Some(path) = &self.0 {
let _ = std::fs::remove_dir(path);
}
}
}

fn root(tool: Harness) -> Result<PathBuf, String> {
let name = match tool {
Harness::CodexCli => "codex",
Harness::Claude => "claude",
_ => return Err("Named accounts are available for Codex CLI and Claude Code.".into()),
};
let home = crate::config::expand_home(Path::new("~"))?;
let mut path = home;
for component in [".switchyard", "accounts", name] {
path.push(component);
if std::fs::symlink_metadata(&path).is_ok_and(|m| m.file_type().is_symlink()) {
return Err(format!(
"{} is a symlink. Edit its target yourself.",
path.display()
));
}
}
Ok(path)
}

pub fn validate(tool: Harness, path: &Path) -> Result<(), String> {
let root = root(tool)?;
if path.parent() != Some(root.as_path())
|| !std::fs::symlink_metadata(path).is_ok_and(|m| m.is_dir() && !m.file_type().is_symlink())
{
return Err("Choose a supported harness.".into());
}
Ok(())
}

pub fn discover(tool: Harness) -> Result<Vec<(String, PathBuf)>, String> {
if !matches!(tool, Harness::CodexCli | Harness::Claude) {
return Ok(Vec::new());
}
let root = root(tool)?;
let entries = match std::fs::read_dir(&root) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(format!(
"Could not list accounts in {}: {error}",
root.display()
));
}
};
let mut accounts = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| error.to_string())?;
if !entry
.file_name()
.to_string_lossy()
.starts_with(".archived-")
&& entry
.file_type()
.map_err(|error| error.to_string())?
.is_dir()
&& let Ok(name) = entry.file_name().into_string()
{
accounts.push((name, entry.path()));
}
}
accounts.sort_by(|a, b| a.0.cmp(&b.0));
Ok(accounts)
}

pub fn list(tool: Harness) -> Vec<(String, PathBuf)> {
discover(tool).unwrap_or_default()
}

pub fn config_paths(tool: Harness, account: Option<&Path>) -> Result<Vec<PathBuf>, String> {
let Some(account) = account else {
return harness::paths(tool);
};
match tool {
Harness::CodexCli => Ok(vec![account.join("sy.config.toml")]),
Harness::Claude => Ok(vec![account.join("settings.json")]),
_ => harness::paths(tool),
}
}

pub fn add(tool: Harness, name: &str) -> Result<String, String> {
if name.is_empty()
|| name.len() > 64
|| !name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err("Account names need 1–64 letters, numbers, hyphens, or underscores.".into());
}
let root = root(tool)?;
harness::binary(tool).ok_or("Install the selected harness first.")?;
std::fs::create_dir_all(&root).map_err(|e| e.to_string())?;
let path = root.join(name);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
std::fs::DirBuilder::new()
.mode(0o700)
.create(&path)
.map_err(|e| {
format!(
"Create account: {e}. \
Pick an existing account after Refresh, or choose a new name."
)
})?;
}
#[cfg(not(unix))]
std::fs::create_dir(&path).map_err(|e| e.to_string())?;
let mut pending = PendingAccount(Some(path.clone()));
login(tool, &path, name)?;
pending.0 = None;
Ok(format!(
"Opened the harness's login in Terminal for account {name}. Complete login, \
click Refresh, and select this account. Switchyard does not read the login tokens. \
The selected account applies to new sessions only. Account directory: {}",
path.display()
))
}

pub fn login(tool: Harness, path: &Path, name: &str) -> Result<String, String> {
validate(tool, path)?;
let binary = harness::binary(tool).ok_or("Install the selected harness first.")?;
let variable = match tool {
Harness::CodexCli => "CODEX_HOME",
Harness::Claude => "CLAUDE_CONFIG_DIR",
_ => return Err("Choose a supported harness.".into()),
};
#[cfg(windows)]
crate::windows::open_terminal(crate::windows::TerminalJob {
binary: binary.clone(),
args: match tool {
Harness::CodexCli => vec!["login".into()],
_ => vec!["auth".into(), "login".into()],
},
directory: None,
environment: vec![(variable.into(), path.display().to_string())],
remove_environment: vec![],
})?;
#[cfg(not(windows))]
{
let login = match tool {
Harness::CodexCli => "login",
_ => "auth login",
};
let script = format!(
"#!/usr/bin/env bash\n\
rm -- \"$0\"\n\
export PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\n\
export {variable}={}\n\
{} {login}",
crate::sessions::quote(&path.display().to_string()),
crate::sessions::quote(&binary.display().to_string())
);
crate::server::open_terminal_script("switchyard-login-", &script)?;
}
Ok(format!(
"Opened login in Terminal for account {name}. Finish login in the harness. \
Login status remains unknown until the harness confirms it."
))
}

pub fn archive(tool: Harness, path: &Path) -> Result<String, String> {
validate(tool, path)?;
let archived = path.with_file_name(format!(".archived-{}", uuid::Uuid::now_v7()));
std::fs::rename(path, &archived).map_err(|error| error.to_string())?;
Ok(format!(
"Archived the account folder at {}. Login files are preserved. \
Existing sessions may still use the old directory and need a new login.",
archived.display()
))
}

#[cfg(all(test, unix))]
mod tests {
use super::*;

// The subprocess isolates HOME so parallel tests keep their own settings.
#[cfg(unix)]
#[test]
fn symlinked_account_roots_are_not_listed_or_used_for_login() {
if std::env::var_os("SWITCHYARD_ACCOUNT_FIXTURE").is_some() {
assert!(list(Harness::CodexCli).is_empty());
assert!(discover(Harness::CodexCli).is_err());
assert!(add(Harness::CodexCli, "new").is_err());
let path = crate::config::expand_home(Path::new("~/.switchyard/accounts/codex/named"))
.expect("home");
assert!(validate(Harness::CodexCli, &path).is_err());
return;
}
let dir = tempfile::tempdir().expect("fixture");
let home = dir.path().join("home");
let target = dir.path().join("target");
std::fs::create_dir(&home).expect("home");
std::fs::create_dir_all(target.join("accounts/codex/named")).expect("accounts");
std::os::unix::fs::symlink(&target, home.join(".switchyard")).expect("symlink");
let output = std::process::Command::new(std::env::current_exe().expect("runner"))
.args([
"--exact",
"accounts::tests::symlinked_account_roots_are_not_listed_or_used_for_login",
])
.env("SWITCHYARD_ACCOUNT_FIXTURE", "1")
.env("HOME", &home)
.output()
.expect("child");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stdout)
);
assert!(!target.join("accounts/codex/new").exists());
}
}
Loading
Loading