Read a timeout in units, share one usage answer, and let a tilde mean home
The composer's settings row is outlined bubbles opening round menus, and the message box is a TextFieldValue so anything put into it without being typed -- a draft, a share, a slash command -- leaves the cursor at the end. A tap that puts a text selection away no longer also collapses the card the text was drawn in: every open and close on the session screen goes through one guard that spends such a press on the selection. The usage bar and the usage dialog were two polls of one measurement and disagreed for up to a minute at a time; they are one feed now, and the countdown rounds up to the minute in the one place both read. A working directory typed as ~/repos/ai-app was four literal characters on the local transport and as an argument on both, so the existence check refused every home-relative path. It is checked by entering the directory now, expanded for a local spawn the way the remote shell expands it, and stored short so the phone draws what somebody would write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2970a6cf9a
commit
32b2a4871a
16 files changed
+414
-99
No files matched your search
+59
-2
@@ -10,7 +10,7 @@
|
||||
//! `~/.ssh/config`, agents, and jump hosts all keep working and there is
|
||||
//! only one place to configure connections (PLAN.md, rule 23).
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::config::SshConfig;
|
||||
@@ -50,7 +50,15 @@ pub fn command(
|
||||
let mut command = Command::new(program);
|
||||
command.args(args);
|
||||
if let Some(cwd) = cwd {
|
||||
command.current_dir(cwd);
|
||||
// Expanded here for the same reason `quote_path` expands it on
|
||||
// the far side: a working directory typed as `~/repos/ai-app`
|
||||
// has to mean the same thing whichever machine runs it. There
|
||||
// is no shell in this branch, so nothing else would --
|
||||
// `current_dir` would be handed the literal one-character
|
||||
// directory `~`, and the session would fail to start with an
|
||||
// error naming a path nobody typed. Only the cwd, matching
|
||||
// the remote side, where arguments stay literal.
|
||||
command.current_dir(expand_home(cwd));
|
||||
}
|
||||
return command;
|
||||
};
|
||||
@@ -101,6 +109,30 @@ fn remote_script(program: &str, args: &[String], cwd: Option<&Path>) -> String {
|
||||
script
|
||||
}
|
||||
|
||||
/// A path with a leading `~` replaced by this machine's home directory.
|
||||
///
|
||||
/// The local half of the rule [`quote_path`] states for the remote one, and
|
||||
/// the two are deliberately the same shape: the tilde is expanded, `~user`
|
||||
/// is not (there is no portable expansion for another account's home), and
|
||||
/// nothing else in the path gains a meaning. A machine with no home
|
||||
/// directory at all leaves the path alone, which fails with the operating
|
||||
/// system's own message rather than with a guess.
|
||||
pub(crate) fn expand_home(path: &Path) -> PathBuf {
|
||||
let Some(rest) = path.to_str().and_then(|p| {
|
||||
if p == "~" {
|
||||
Some("")
|
||||
} else {
|
||||
p.strip_prefix("~/")
|
||||
}
|
||||
}) else {
|
||||
return path.to_path_buf();
|
||||
};
|
||||
match std::env::home_dir() {
|
||||
Some(home) => home.join(rest),
|
||||
None => path.to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Quotes a path, expanding a leading `~` and nothing else.
|
||||
///
|
||||
/// [`quote`] is right for every other word crossing to the remote side and
|
||||
@@ -247,6 +279,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The same character, on the transport with no shell to expand it.
|
||||
///
|
||||
/// The local branch runs the program directly, so a working directory
|
||||
/// of `~/repos/ai-app` would reach `current_dir` as the literal
|
||||
/// one-character directory `~` -- a session that fails to start,
|
||||
/// naming a path nobody typed. The two transports have to agree about
|
||||
/// what a tilde means or a path is only portable by accident.
|
||||
#[test]
|
||||
fn a_local_cwd_expands_its_tilde_the_same_way() {
|
||||
let Some(home) = std::env::home_dir() else {
|
||||
return;
|
||||
};
|
||||
assert_eq!(
|
||||
expand_home(Path::new("~/repos/ai-app")),
|
||||
home.join("repos/ai-app")
|
||||
);
|
||||
assert_eq!(expand_home(Path::new("~")), home);
|
||||
// Leading only, and its own segment only -- `quote_path`'s rule.
|
||||
assert_eq!(expand_home(Path::new("/tmp/~/x")), Path::new("/tmp/~/x"));
|
||||
assert_eq!(expand_home(Path::new("~user/x")), Path::new("~user/x"));
|
||||
|
||||
let local = command(None, "claude", &args(["-p"]), Some(Path::new("~/work")));
|
||||
assert_eq!(local.get_current_dir(), Some(home.join("work").as_path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_metacharacters_cross_as_data_not_syntax() {
|
||||
// Expanding $HOME must not open a door for anything else: the rest
|
||||
|
||||
Reference in new issue
Block a user