Synced from monorepo

Changes:
- Stop hooks for session lifecycle
- Add x.ai/session/state and x.ai/session/import ACP methods
- Deny-and-continue for auto-mode classifier blocks with denial limits
- Drop codebase-upload from dhat soak test
- scheduler_create upsert via task_id; retire one-shot tasks
- Clipboard: copy file fallback + honest toasts for SSH/Apple Terminal
- Polarity-safe syntax colors in minimal mode
- Auto mode classifies unvetted env prefixes instead of hard-prompting
- Add GROK_CLIPBOARD_NO_OSC52 kill switch to force OSC 52 off
This commit is contained in:
grokkybara[bot] 2026-07-19 18:40:33 +01:00
commit ba76b0a683
143 changed files with 9465 additions and 3419 deletions

View file

@ -1,9 +1,17 @@
//! `/copy` -- copy the last (or Nth) assistant message to the clipboard.
//!
//! Optional file path writes instead of (or when) the clipboard is unreachable:
//! - `/copy` — latest → clipboard (file fallback on failure)
//! - `/copy 2` — 2nd-latest → clipboard
//! - `/copy out.txt` — latest → file
//! - `/copy 2 out.txt` — 2nd-latest → file
use std::path::PathBuf;
use crate::app::actions::Action;
use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand};
/// Copy an assistant message to the clipboard.
/// Copy an assistant message to the clipboard (or an optional file).
pub struct CopyCommand;
impl SlashCommand for CopyCommand {
@ -12,7 +20,7 @@ impl SlashCommand for CopyCommand {
}
fn description(&self) -> &str {
"Copy last response to clipboard (/copy N for Nth-latest)"
"Copy last response to clipboard or file (/copy [N] [file])"
}
fn session_scoped(&self) -> bool {
@ -20,7 +28,7 @@ impl SlashCommand for CopyCommand {
}
fn usage(&self) -> &str {
"/copy [N]"
"/copy [N] [file]"
}
fn takes_args(&self) -> bool {
@ -28,29 +36,42 @@ impl SlashCommand for CopyCommand {
}
fn arg_placeholder(&self) -> Option<&str> {
Some("[N]")
Some("[N] [file]")
}
fn run(&self, _ctx: &mut CommandExecCtx, args: &str) -> CommandResult {
let trimmed = args.trim();
let n = if trimmed.is_empty() {
1
} else {
match trimmed.parse::<usize>() {
Ok(0) => {
return CommandResult::Error(
"Usage: /copy [N] where N is 1 (latest), 2, 3, ...".to_string(),
);
}
Ok(v) => v,
Err(_) => {
return CommandResult::Error(format!(
"/copy {trimmed} (invalid number)\nUsage: /copy [N] where N is 1 (latest), 2, 3, ..."
));
}
match parse_copy_args(args) {
Ok((n, file_path)) => {
CommandResult::Action(Action::CopyAssistantMessage { n, file_path })
}
};
CommandResult::Action(Action::CopyAssistantMessage { n })
Err(msg) => CommandResult::Error(msg),
}
}
}
/// Parse `/copy` args into `(n, optional_file_path)`.
///
/// - empty → `(1, None)`
/// - `2` → `(2, None)`
/// - `out.txt` → `(1, Some(out.txt))`
/// - `2 out.txt` → `(2, Some(out.txt))` (rest of line is the path, spaces ok)
fn parse_copy_args(args: &str) -> Result<(usize, Option<PathBuf>), String> {
let trimmed = args.trim();
if trimmed.is_empty() {
return Ok((1, None));
}
let mut parts = trimmed.splitn(2, char::is_whitespace);
let first = parts.next().unwrap_or("");
let rest = parts.next().map(str::trim).filter(|s| !s.is_empty());
match first.parse::<usize>() {
Ok(0) => Err("Usage: /copy [N] [file] where N is 1 (latest), 2, 3, ...".to_string()),
Ok(n) => Ok((n, rest.map(PathBuf::from))),
Err(_) => {
// Non-numeric first token: treat the whole args string as a path.
Ok((1, Some(PathBuf::from(trimmed))))
}
}
}
@ -88,7 +109,10 @@ mod tests {
let mut ctx = make_ctx(&models);
let cmd = CopyCommand;
match cmd.run(&mut ctx, "") {
CommandResult::Action(Action::CopyAssistantMessage { n }) => assert_eq!(n, 1),
CommandResult::Action(Action::CopyAssistantMessage { n, file_path }) => {
assert_eq!(n, 1);
assert!(file_path.is_none());
}
other => panic!("expected Action(CopyAssistantMessage), got {other:?}"),
}
}
@ -99,7 +123,10 @@ mod tests {
let mut ctx = make_ctx(&models);
let cmd = CopyCommand;
match cmd.run(&mut ctx, "1") {
CommandResult::Action(Action::CopyAssistantMessage { n }) => assert_eq!(n, 1),
CommandResult::Action(Action::CopyAssistantMessage { n, file_path }) => {
assert_eq!(n, 1);
assert!(file_path.is_none());
}
other => panic!("expected Action(CopyAssistantMessage), got {other:?}"),
}
}
@ -110,7 +137,10 @@ mod tests {
let mut ctx = make_ctx(&models);
let cmd = CopyCommand;
match cmd.run(&mut ctx, "3") {
CommandResult::Action(Action::CopyAssistantMessage { n }) => assert_eq!(n, 3),
CommandResult::Action(Action::CopyAssistantMessage { n, file_path }) => {
assert_eq!(n, 3);
assert!(file_path.is_none());
}
other => panic!("expected Action(CopyAssistantMessage), got {other:?}"),
}
}
@ -124,23 +154,37 @@ mod tests {
}
#[test]
fn non_numeric_returns_error() {
fn path_only_writes_latest_to_file() {
let models = ModelState::default();
let mut ctx = make_ctx(&models);
let cmd = CopyCommand;
match cmd.run(&mut ctx, "abc") {
CommandResult::Error(msg) => assert!(msg.contains("invalid number")),
other => panic!("expected Error, got {other:?}"),
match cmd.run(&mut ctx, "out.txt") {
CommandResult::Action(Action::CopyAssistantMessage { n, file_path }) => {
assert_eq!(n, 1);
assert_eq!(file_path.as_deref(), Some(std::path::Path::new("out.txt")));
}
other => panic!("expected Action(CopyAssistantMessage), got {other:?}"),
}
}
#[test]
fn n_and_path_with_spaces() {
assert_eq!(
parse_copy_args("2 ~/exports/my note.txt").unwrap(),
(2, Some(PathBuf::from("~/exports/my note.txt")))
);
}
#[test]
fn whitespace_only_copies_latest() {
let models = ModelState::default();
let mut ctx = make_ctx(&models);
let cmd = CopyCommand;
match cmd.run(&mut ctx, " ") {
CommandResult::Action(Action::CopyAssistantMessage { n }) => assert_eq!(n, 1),
CommandResult::Action(Action::CopyAssistantMessage { n, file_path }) => {
assert_eq!(n, 1);
assert!(file_path.is_none());
}
other => panic!("expected Action(CopyAssistantMessage), got {other:?}"),
}
}