1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! Which account a launch runs as.
//!
//! An account is a directory the harness is pointed at by an environment
//! variable, and only harnesses that read such a variable can have one — so the
//! question "is there a profile for this command" has to be answered from the
//! command itself, before anything is spawned. That lookup is shared by the
//! launcher's cycling, the tab border's label, and the argv actually run.
use super::*;
impl App {
/// Open the add-account popup on its first step, the name.
pub(super) fn open_add_account(&mut self) {
self.add_account = AddAccount::default();
self.mode = Mode::AddAccount;
self.needs_redraw = true;
}
/// The name is in: start `claude setup-token` inside the popup.
pub(super) fn start_setup_token(&mut self) {
let name = self.add_account.name.trim().to_string();
if !crate::quota::valid_account_name(&name) {
self.set_status("An account name is letters, digits, - _ and . only");
return;
}
self.add_account.name = name;
let argv = ["claude".to_string(), "setup-token".to_string()];
match tabs::Pane::launch(&argv, None, tabs::Own::Cctop) {
Ok(pane) => self.add_account.pane = Some(pane),
// A `claude` that cannot be started here: the command-line
// walkthrough does the same job and says what went wrong.
Err(e) => {
self.add_account.outcome = Some(Err(format!(
"Could not run `claude setup-token` here ({e}). In a terminal, \
`cctop --add-account {}` walks through the same steps.",
self.add_account.name
)))
}
}
self.needs_redraw = true;
}
/// Feed the popup's terminal, and take the token the moment it is printed.
pub(super) fn pump_add_account(&mut self) {
let flow = &mut self.add_account;
let Some(pane) = flow.pane.as_mut() else {
return;
};
if pane.view.pump() {
self.needs_redraw = true;
let screen = pane.view.parser.screen();
if flow.link.is_none() {
flow.link = crate::quota::link_on_screen(screen);
}
if let Some(token) = crate::quota::token_on_screen(screen) {
// Its job is done, and a process holding a fresh token has no
// reason to outlive the popup that asked for it.
flow.pane = None;
flow.outcome = Some(
crate::quota::save_token(&flow.name, &token)
.map(|()| flow.name.clone())
.map_err(|e| format!("Could not save the token: {e}")),
);
return;
}
}
// Left on screen rather than dropped: whatever it said before it went
// is the explanation.
if flow.outcome.is_none() && pane.view.closed() {
self.needs_redraw = true;
flow.outcome = Some(Err(
"`claude setup-token` ended without printing a token.".to_string()
));
}
}
/// The profile a launch would use, or `None` when the highlighted command
/// takes none — or takes one but has only a single account, so there is
/// nothing to choose between.
pub fn launch_profile(&self) -> Option<&'static crate::config::Profile> {
self.chosen_profile(self.launch_provider()?)
}
/// Which harness the highlighted choice would start, when it is one whose
/// account cctop can pick.
pub(super) fn launch_provider(&self) -> Option<Provider> {
match self.launch_offer.get(self.launch_cursor) {
Some(tabs::Choice::Start(argv)) => Self::profile_provider(argv),
_ => None,
}
}
/// The profile `provider` would be started under, or `None` when it has
/// only the one and so nothing to choose between.
pub fn chosen_profile(&self, provider: Provider) -> Option<&'static crate::config::Profile> {
let profiles = crate::config::launchable_for(provider);
if profiles.len() <= 1 {
return None;
}
let at = self.launch_profile.get(&provider).copied().unwrap_or(0);
profiles.get(at).copied()
}
/// Move to the next profile of the highlighted harness. Wraps, because with
/// two — which is the case this exists for — a key that toggles is the whole
/// interaction.
pub(super) fn cycle_launch_profile(&mut self) {
let Some(provider) = self.launch_provider() else {
return;
};
let n = crate::config::launchable_for(provider).len();
if n > 1 {
let at = self.launch_profile.entry(provider).or_insert(0);
*at = (*at + 1) % n;
self.save_prefs();
self.needs_redraw = true;
}
}
/// Which harness `argv` starts, when it is one whose account is selected by
/// an environment variable — and so one a profile means something to.
///
/// `$CLAUDE_CONFIG_DIR` is Claude's and `$CODEX_HOME` is Codex's; putting
/// either in front of anything else would be a promise the env var cannot
/// keep.
pub(super) fn profile_provider(argv: &[String]) -> Option<Provider> {
let command = argv
.first()
.map(|c| c.rsplit(['/', '\\']).next().unwrap_or(c))?;
match command.strip_suffix(".exe").unwrap_or(command) {
"claude" => Some(Provider::Claude),
"codex" => Some(Provider::Codex),
_ => None,
}
}
/// Put the chosen profile in front of the command that will read it.
///
/// `env VAR=value cmd` rather than plumbing an environment through every
/// spawn path: the same argv is handed to rmux, to a pty cctop owns, and to
/// `rmux new-session`, and `env` is understood identically by all three.
/// [`tabs::label_of`] drops the prefix again so the tab is named after the
/// agent rather than after how it was started.
pub(super) fn with_profile(&self, argv: Vec<String>) -> Vec<String> {
let profile = Self::profile_provider(&argv).and_then(|p| self.chosen_profile(p));
let Some(profile) = profile else {
return argv;
};
crate::config::argv_under_profile(argv, profile)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Which harness an argv names, and so which variable may be put in front
/// of it. Getting this wrong is not a cosmetic error: `CODEX_HOME` in front
/// of `claude` is ignored, and the agent then runs as an account the pane
/// says it is not.
#[test]
fn only_a_harness_with_a_config_variable_takes_a_profile() {
let of = |c: &str| App::profile_provider(&[c.to_string()]);
assert_eq!(of("claude"), Some(Provider::Claude));
assert_eq!(of("codex"), Some(Provider::Codex));
// Found by basename, however it was spelled on the way in.
assert_eq!(of("/usr/local/bin/codex"), Some(Provider::Codex));
assert_eq!(of("codex.exe"), Some(Provider::Codex));
assert_eq!(of(r"C:\tools\claude.exe"), Some(Provider::Claude));
// No such variable: a profile would be a setting that did nothing.
assert_eq!(of("cursor-agent"), None);
assert_eq!(of("gemini"), None);
// Not a prefix match — `codex-something` is not codex.
assert_eq!(of("codexa"), None);
assert_eq!(App::profile_provider(&[]), None);
}
/// The prefix a launch actually carries, per harness.
#[test]
fn a_profile_reaches_the_agent_as_its_own_variable() {
let under = |dir: &str, provider, command: &str| {
let profile = crate::config::Profile {
provider,
name: "work".to_string(),
dir: std::path::PathBuf::from(dir),
source: crate::config::AccountSource::Directory,
};
crate::config::argv_under_profile(vec![command.to_string()], &profile)
};
assert_eq!(
under("/home/x/.codex-work", Provider::Codex, "codex"),
["env", "CODEX_HOME=/home/x/.codex-work", "codex"]
);
assert_eq!(
under("/home/x/.claude-work", Provider::Claude, "claude"),
["env", "CLAUDE_CONFIG_DIR=/home/x/.claude-work", "claude"]
);
// A harness with no variable is handed its command untouched rather
// than an `env` prefix that promises something.
assert_eq!(
under("/home/x/.cursor", Provider::Cursor, "cursor-agent"),
["cursor-agent"]
);
}
}