Synced from monorepo
Synced from monorepo Changes: - Shell: accept target response id on rewind execute - Shell: stamp response id on chat user message chunks - Worktree: optional rebuild and stale git registration cleanup in auto-GC - Worktree: kind-aware auto-GC TTLs and config knobs - Worktree: macOS process CWD scan and Unix PID liveness for GC guards - Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only) - Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups - Shell: stop overwriting user skills - Tools: read markdown in `skills/` directories untruncated - `/usage` shows per-session token and dollar usage in the TUI - Security: prompt on environment-dumping `ps` variants - Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission - Tools: make scheduler deletion durable - Shell: add relocation storage primitives - Shell: give side model calls their own conversation ids - Fix five workflow-runtime bugs (budget, pause, cancel, reconnect) - Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask) - Pager: expose doctor in the TUI - Security: block unauthorized RCE via abused safe commands - Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent" - Security: block `rg --pre` arbitrary code execution in auto-mode - Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section - App builder deployer: `allow_forking` and `show_built_with_grok` - Pager: stop stacking duplicate "Worked for" markers on parked turns - Shell: support `max` as a distinct reasoning effort tier - Tools: serialize background `/loop` fires on the whole work unit - Shell: add working-directory relocation state primitives - Proto: `ClientToolResult` and `ChatConfig` client-side tools - Shell: model providers - Chat: select App Builder product on the Build path - Shell: attach author identity to feedback when the deployment opts in - Doctor: fix for SSH wrap setup - Workflow authoring skills: create-workflow and import-claude-workflow docs - Add read-only grok doctor - Sandbox: apply Landlock without a controlling TTY - Pager: recover image paste over grok wrap on headless remotes - Pager: make actions screen-mode aware - Shell: resume sessions when the working directory moves - Pager: centralize terminal diagnostics - Workspace: gate inline shell file access - Pager: centralize terminal probes - Pager: edit minimal prompts in an external editor - Pager: standardize backgrounding on Ctrl+B - Shell: recap rides the parent turn's prompt cache - Tools: add scheduler lifecycle version clock Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
This commit is contained in:
parent
a881e6703f
commit
3af4d5d398
556 changed files with 56609 additions and 21892 deletions
274
crates/codegen/xai-grok-pager-render/src/terminal/tmux_probe.rs
Normal file
274
crates/codegen/xai-grok-pager-render/src/terminal/tmux_probe.rs
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
//! Shared tmux command protocol and result parsing.
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum TmuxCommand<'a> {
|
||||
Version,
|
||||
OptionValue(&'a str),
|
||||
OptionSupport(&'a str),
|
||||
ControlMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct TmuxCommandOutput {
|
||||
status_success: bool,
|
||||
stdout: Vec<u8>,
|
||||
stderr: Vec<u8>,
|
||||
}
|
||||
|
||||
trait TmuxCommandRunner {
|
||||
fn run(&self, command: TmuxCommand<'_>) -> Result<TmuxCommandOutput, String>;
|
||||
}
|
||||
|
||||
struct LiveTmuxCommandRunner;
|
||||
|
||||
impl TmuxCommandRunner for LiveTmuxCommandRunner {
|
||||
fn run(&self, command: TmuxCommand<'_>) -> Result<TmuxCommandOutput, String> {
|
||||
let output = build_tmux_command(command)
|
||||
.output()
|
||||
.map_err(|error| format!("failed to run tmux: {error}"))?;
|
||||
Ok(TmuxCommandOutput {
|
||||
status_success: output.status.success(),
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TmuxQueryResult<T> {
|
||||
Available(T),
|
||||
Unsupported,
|
||||
Unavailable,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl<T> TmuxQueryResult<T> {
|
||||
pub fn into_option(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Available(value) => Some(value),
|
||||
Self::Unsupported | Self::Unavailable | Self::Error(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_version() -> TmuxQueryResult<String> {
|
||||
query_version_with(&LiveTmuxCommandRunner)
|
||||
}
|
||||
|
||||
fn query_version_with(runner: &dyn TmuxCommandRunner) -> TmuxQueryResult<String> {
|
||||
parse_value(runner.run(TmuxCommand::Version))
|
||||
}
|
||||
|
||||
pub fn query_option(option: &str) -> TmuxQueryResult<String> {
|
||||
query_option_with(&LiveTmuxCommandRunner, option)
|
||||
}
|
||||
|
||||
fn query_option_with(runner: &dyn TmuxCommandRunner, option: &str) -> TmuxQueryResult<String> {
|
||||
parse_value(runner.run(TmuxCommand::OptionValue(option)))
|
||||
}
|
||||
|
||||
pub fn query_option_support(option: &str) -> TmuxQueryResult<()> {
|
||||
query_option_support_with(&LiveTmuxCommandRunner, option)
|
||||
}
|
||||
|
||||
fn query_option_support_with(runner: &dyn TmuxCommandRunner, option: &str) -> TmuxQueryResult<()> {
|
||||
match runner.run(TmuxCommand::OptionSupport(option)) {
|
||||
Ok(output) if output.status_success => TmuxQueryResult::Available(()),
|
||||
Ok(output) if stderr_identifies_unknown_option(&output.stderr, option) => {
|
||||
TmuxQueryResult::Unsupported
|
||||
}
|
||||
Ok(_) => TmuxQueryResult::Unavailable,
|
||||
Err(error) => TmuxQueryResult::Error(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_control_mode() -> TmuxQueryResult<bool> {
|
||||
query_control_mode_with(&LiveTmuxCommandRunner)
|
||||
}
|
||||
|
||||
fn query_control_mode_with(runner: &dyn TmuxCommandRunner) -> TmuxQueryResult<bool> {
|
||||
match runner.run(TmuxCommand::ControlMode) {
|
||||
Ok(output) if output.status_success => TmuxQueryResult::Available(
|
||||
String::from_utf8_lossy(&output.stdout).contains("control-mode"),
|
||||
),
|
||||
Ok(_) => TmuxQueryResult::Unavailable,
|
||||
Err(error) => TmuxQueryResult::Error(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tmux_command(command: TmuxCommand<'_>) -> Command {
|
||||
let mut cmd = Command::new("tmux");
|
||||
match command {
|
||||
TmuxCommand::Version => {
|
||||
cmd.arg("-V").stdout(Stdio::piped()).stderr(Stdio::null());
|
||||
}
|
||||
TmuxCommand::OptionValue(option) => {
|
||||
cmd.args(["show-option", "-gqv", option])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null());
|
||||
}
|
||||
TmuxCommand::OptionSupport(option) => {
|
||||
cmd.args(["show-option", "-gv", option])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped());
|
||||
}
|
||||
TmuxCommand::ControlMode => {
|
||||
cmd.args(["display-message", "-p", "#{client_flags}"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null());
|
||||
}
|
||||
}
|
||||
cmd.stdin(Stdio::null()).envs(xai_tty_utils::pager_env());
|
||||
xai_tty_utils::detach_std_command(&mut cmd);
|
||||
cmd
|
||||
}
|
||||
|
||||
fn parse_value(output: Result<TmuxCommandOutput, String>) -> TmuxQueryResult<String> {
|
||||
match output {
|
||||
Ok(output) if output.status_success => {
|
||||
let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
|
||||
if value.is_empty() {
|
||||
TmuxQueryResult::Unavailable
|
||||
} else {
|
||||
TmuxQueryResult::Available(value)
|
||||
}
|
||||
}
|
||||
Ok(_) => TmuxQueryResult::Unavailable,
|
||||
Err(error) => TmuxQueryResult::Error(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn stderr_identifies_unknown_option(stderr: &[u8], option: &str) -> bool {
|
||||
let invalid = format!("invalid option: {option}");
|
||||
let unknown = format!("unknown option: {option}");
|
||||
String::from_utf8_lossy(stderr)
|
||||
.lines()
|
||||
.any(|line| matches!(line.trim(), value if value == invalid || value == unknown))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct FakeRunner {
|
||||
output: Result<TmuxCommandOutput, String>,
|
||||
calls: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl FakeRunner {
|
||||
fn output(status_success: bool, stdout: &[u8], stderr: &[u8]) -> Self {
|
||||
Self {
|
||||
output: Ok(TmuxCommandOutput {
|
||||
status_success,
|
||||
stdout: stdout.to_vec(),
|
||||
stderr: stderr.to_vec(),
|
||||
}),
|
||||
calls: RefCell::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TmuxCommandRunner for FakeRunner {
|
||||
fn run(&self, command: TmuxCommand<'_>) -> Result<TmuxCommandOutput, String> {
|
||||
self.calls.borrow_mut().push(match command {
|
||||
TmuxCommand::Version => "version".to_owned(),
|
||||
TmuxCommand::OptionValue(option) => format!("value:{option}"),
|
||||
TmuxCommand::OptionSupport(option) => format!("support:{option}"),
|
||||
TmuxCommand::ControlMode => "control-mode".to_owned(),
|
||||
});
|
||||
self.output.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_protocol_uses_exact_argv_and_pager_env() {
|
||||
let cases = [
|
||||
(TmuxCommand::Version, vec!["-V"]),
|
||||
(
|
||||
TmuxCommand::OptionValue("set-clipboard"),
|
||||
vec!["show-option", "-gqv", "set-clipboard"],
|
||||
),
|
||||
(
|
||||
TmuxCommand::OptionSupport("allow-passthrough"),
|
||||
vec!["show-option", "-gv", "allow-passthrough"],
|
||||
),
|
||||
(
|
||||
TmuxCommand::ControlMode,
|
||||
vec!["display-message", "-p", "#{client_flags}"],
|
||||
),
|
||||
];
|
||||
for (request, args) in cases {
|
||||
let cmd = build_tmux_command(request);
|
||||
assert_eq!(cmd.get_program(), OsStr::new("tmux"));
|
||||
assert_eq!(cmd.get_args().collect::<Vec<_>>(), args);
|
||||
let actual: HashMap<OsString, Option<OsString>> = cmd
|
||||
.get_envs()
|
||||
.map(|(key, value)| (key.to_owned(), value.map(OsStr::to_owned)))
|
||||
.collect();
|
||||
let expected: HashMap<OsString, Option<OsString>> = xai_tty_utils::pager_env()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.into(), Some(value.into())))
|
||||
.collect();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_and_control_queries_use_the_injected_runner() {
|
||||
let runner = FakeRunner::output(true, b" on\n", b"");
|
||||
assert_eq!(
|
||||
query_option_with(&runner, "set-clipboard"),
|
||||
TmuxQueryResult::Available("on".to_owned())
|
||||
);
|
||||
assert_eq!(runner.calls.into_inner(), ["value:set-clipboard"]);
|
||||
|
||||
let runner = FakeRunner::output(true, b"control-mode,utf8\n", b"");
|
||||
assert_eq!(
|
||||
query_control_mode_with(&runner),
|
||||
TmuxQueryResult::Available(true)
|
||||
);
|
||||
assert_eq!(runner.calls.into_inner(), ["control-mode"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonzero_and_execution_failure_remain_fail_open_facts() {
|
||||
let runner = FakeRunner::output(false, b"on", b"server unavailable");
|
||||
assert_eq!(
|
||||
query_option_with(&runner, "set-clipboard"),
|
||||
TmuxQueryResult::Unavailable
|
||||
);
|
||||
let runner = FakeRunner {
|
||||
output: Err("spawn failed".to_owned()),
|
||||
calls: RefCell::new(Vec::new()),
|
||||
};
|
||||
assert_eq!(
|
||||
query_version_with(&runner),
|
||||
TmuxQueryResult::Error("spawn failed".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn support_query_accepts_both_known_spellings_only() {
|
||||
for stderr in [
|
||||
b"invalid option: allow-passthrough\n".as_slice(),
|
||||
b"unknown option: allow-passthrough\n".as_slice(),
|
||||
] {
|
||||
let runner = FakeRunner::output(false, b"", stderr);
|
||||
assert_eq!(
|
||||
query_option_support_with(&runner, "allow-passthrough"),
|
||||
TmuxQueryResult::Unsupported
|
||||
);
|
||||
}
|
||||
let runner = FakeRunner::output(false, b"", b"no server running\n");
|
||||
assert_eq!(
|
||||
query_option_support_with(&runner, "allow-passthrough"),
|
||||
TmuxQueryResult::Unavailable
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue