mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(tui): add upstream-compatible slash commands
This commit is contained in:
@@ -615,6 +615,27 @@ impl App<'_> {
|
||||
self.input_running.store(false, Ordering::Release);
|
||||
break 'main;
|
||||
}
|
||||
AppEvent::ClearUi => {
|
||||
terminal.clear()?;
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.abort_active_turn_for_new_chat();
|
||||
}
|
||||
let mut new_widget = ChatWidget::new(
|
||||
self.config.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
None,
|
||||
Vec::new(),
|
||||
self.enhanced_keys_supported,
|
||||
self.terminal_info.clone(),
|
||||
self.show_order_overlay,
|
||||
self.latest_upgrade_version.clone(),
|
||||
);
|
||||
new_widget.enable_perf(self.timing_enabled);
|
||||
new_widget.set_standard_terminal_mode(!self.alt_screen_active);
|
||||
self.app_state = AppState::Chat { widget: Box::new(new_widget) };
|
||||
self.terminal_runs.clear();
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
AppEvent::CancelRunningTask => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.cancel_running_task_from_approval();
|
||||
@@ -1179,7 +1200,24 @@ impl App<'_> {
|
||||
self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
|
||||
}
|
||||
}
|
||||
SlashCommand::Quit => { break 'main; }
|
||||
SlashCommand::Quit | SlashCommand::Exit => { break 'main; }
|
||||
SlashCommand::Clear => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
if widget.is_task_running() {
|
||||
widget.history_push_plain_state(history_cell::new_error_event(
|
||||
"'/clear' is disabled while a task is in progress.".to_string(),
|
||||
));
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
} else {
|
||||
self.app_event_tx.send(AppEvent::ClearUi);
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Copy => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.copy_last_agent_markdown();
|
||||
}
|
||||
}
|
||||
SlashCommand::Login => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_login_command();
|
||||
|
||||
@@ -181,6 +181,9 @@ pub(crate) enum AppEvent {
|
||||
/// Request to exit the application gracefully.
|
||||
ExitRequest,
|
||||
|
||||
/// Clear the terminal UI and start a fresh chat.
|
||||
ClearUi,
|
||||
|
||||
/// Forward an `Op` to the Agent. Using an `AppEvent` for this avoids
|
||||
/// bubbling channels through layers of widgets.
|
||||
CodexOp(code_core::protocol::Op),
|
||||
|
||||
@@ -48,6 +48,7 @@ impl AppEventSender {
|
||||
| AppEvent::RequestRedraw
|
||||
| AppEvent::Redraw
|
||||
| AppEvent::ExitRequest
|
||||
| AppEvent::ClearUi
|
||||
| AppEvent::SetTerminalTitle { .. }
|
||||
| AppEvent::EmitTuiNotification { .. }
|
||||
| AppEvent::AutoCoordinatorCountdown { .. }
|
||||
|
||||
@@ -1856,6 +1856,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
login_add_view_state: Option<Weak<RefCell<LoginAddAccountState>>>,
|
||||
active_exec_cell: Option<ExecCell>,
|
||||
history_cells: Vec<Box<dyn HistoryCell>>, // Store all history in memory
|
||||
clipboard_lease: Option<crate::clipboard_copy::ClipboardLease>,
|
||||
history_cell_ids: Vec<Option<HistoryId>>,
|
||||
history_live_window: Option<(usize, usize)>,
|
||||
history_frozen_width: u16,
|
||||
@@ -6979,6 +6980,7 @@ impl ChatWidget<'_> {
|
||||
login_add_view_state: None,
|
||||
active_exec_cell: None,
|
||||
history_cells,
|
||||
clipboard_lease: None,
|
||||
config: config.clone(),
|
||||
mcp_tools_by_server: HashMap::new(),
|
||||
mcp_server_failures: HashMap::new(),
|
||||
@@ -7354,6 +7356,7 @@ impl ChatWidget<'_> {
|
||||
login_add_view_state: None,
|
||||
active_exec_cell: None,
|
||||
history_cells,
|
||||
clipboard_lease: None,
|
||||
config: config.clone(),
|
||||
mcp_tools_by_server: HashMap::new(),
|
||||
mcp_server_failures: HashMap::new(),
|
||||
@@ -17125,6 +17128,43 @@ impl ChatWidget<'_> {
|
||||
self.show_settings_overlay(Some(SettingsSection::Skills));
|
||||
}
|
||||
|
||||
/// Copy the last assistant response (raw markdown) to the system clipboard.
|
||||
pub(crate) fn copy_last_agent_markdown(&mut self) {
|
||||
self.copy_last_agent_markdown_with(crate::clipboard_copy::copy_to_clipboard);
|
||||
}
|
||||
|
||||
fn copy_last_agent_markdown_with(
|
||||
&mut self,
|
||||
copy_fn: impl FnOnce(&str) -> Result<Option<crate::clipboard_copy::ClipboardLease>, String>,
|
||||
) {
|
||||
let markdown = self.history_cells.iter().rev().find_map(|cell| {
|
||||
cell.as_any()
|
||||
.downcast_ref::<history_cell::AssistantMarkdownCell>()
|
||||
.map(|assistant| assistant.markdown().to_string())
|
||||
.filter(|text| !text.is_empty())
|
||||
});
|
||||
|
||||
match markdown {
|
||||
Some(markdown) => match copy_fn(&markdown) {
|
||||
Ok(lease) => {
|
||||
self.clipboard_lease = lease;
|
||||
self.history_push_plain_state(history_cell::plain_message_state_from_lines(
|
||||
vec![Line::from("Copied last message to clipboard")],
|
||||
crate::history_cell::HistoryCellType::Notice,
|
||||
));
|
||||
}
|
||||
Err(error) => self.history_push_plain_state(history_cell::new_error_event(
|
||||
format!("Copy failed: {error}"),
|
||||
)),
|
||||
},
|
||||
None => self.history_push_plain_state(history_cell::new_error_event(
|
||||
"No agent response to copy".to_string(),
|
||||
)),
|
||||
}
|
||||
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn add_agents_output(&mut self) {
|
||||
use ratatui::text::Line;
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
//! Clipboard copy backend for the TUI's `/copy` command.
|
||||
//!
|
||||
//! The selection order mirrors upstream Codex: SSH uses OSC 52, local sessions
|
||||
//! try the native clipboard first, WSL falls back to PowerShell, then OSC 52.
|
||||
|
||||
use base64::Engine;
|
||||
use std::io::Write;
|
||||
|
||||
const OSC52_MAX_RAW_BYTES: usize = 100_000;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
static STDERR_SUPPRESSION_MUTEX: std::sync::OnceLock<std::sync::Mutex<()>> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
pub(crate) fn copy_to_clipboard(text: &str) -> Result<Option<ClipboardLease>, String> {
|
||||
copy_to_clipboard_with(
|
||||
text,
|
||||
is_ssh_session(),
|
||||
is_wsl_session(),
|
||||
osc52_copy,
|
||||
arboard_copy,
|
||||
wsl_clipboard_copy,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) struct ClipboardLease {
|
||||
#[cfg(target_os = "linux")]
|
||||
_clipboard: Option<arboard::Clipboard>,
|
||||
}
|
||||
|
||||
impl ClipboardLease {
|
||||
#[cfg(target_os = "linux")]
|
||||
fn native_linux(clipboard: arboard::Clipboard) -> Self {
|
||||
Self {
|
||||
_clipboard: Some(clipboard),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test() -> Self {
|
||||
Self {
|
||||
#[cfg(target_os = "linux")]
|
||||
_clipboard: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_to_clipboard_with(
|
||||
text: &str,
|
||||
ssh_session: bool,
|
||||
wsl_session: bool,
|
||||
osc52_copy_fn: impl Fn(&str) -> Result<(), String>,
|
||||
arboard_copy_fn: impl Fn(&str) -> Result<Option<ClipboardLease>, String>,
|
||||
wsl_copy_fn: impl Fn(&str) -> Result<(), String>,
|
||||
) -> Result<Option<ClipboardLease>, String> {
|
||||
if ssh_session {
|
||||
return osc52_copy_fn(text).map(|()| None).map_err(|osc_err| {
|
||||
tracing::warn!("OSC 52 clipboard copy failed over SSH: {osc_err}");
|
||||
format!("OSC 52 clipboard copy failed over SSH: {osc_err}")
|
||||
});
|
||||
}
|
||||
|
||||
match arboard_copy_fn(text) {
|
||||
Ok(lease) => Ok(lease),
|
||||
Err(native_err) => {
|
||||
if wsl_session {
|
||||
tracing::warn!(
|
||||
"native clipboard copy failed: {native_err}, falling back to WSL PowerShell"
|
||||
);
|
||||
match wsl_copy_fn(text) {
|
||||
Ok(()) => return Ok(None),
|
||||
Err(wsl_err) => {
|
||||
tracing::warn!(
|
||||
"WSL PowerShell clipboard copy failed: {wsl_err}, falling back to OSC 52"
|
||||
);
|
||||
return osc52_copy_fn(text).map(|()| None).map_err(|osc_err| {
|
||||
format!(
|
||||
"native clipboard: {native_err}; WSL fallback: {wsl_err}; OSC 52 fallback: {osc_err}"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::warn!("native clipboard copy failed: {native_err}, falling back to OSC 52");
|
||||
osc52_copy_fn(text).map(|()| None).map_err(|osc_err| {
|
||||
format!("native clipboard: {native_err}; OSC 52 fallback: {osc_err}")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ssh_session() -> bool {
|
||||
std::env::var_os("SSH_TTY").is_some() || std::env::var_os("SSH_CONNECTION").is_some()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_wsl_session() -> bool {
|
||||
if std::env::var_os("WSL_DISTRO_NAME").is_some()
|
||||
|| std::env::var_os("WSL_INTEROP").is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::fs::read_to_string("/proc/sys/kernel/osrelease")
|
||||
.map(|release| release.to_ascii_lowercase().contains("microsoft"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn is_wsl_session() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "android"), not(target_os = "linux")))]
|
||||
fn arboard_copy(text: &str) -> Result<Option<ClipboardLease>, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _stderr_lock = STDERR_SUPPRESSION_MUTEX
|
||||
.get_or_init(|| std::sync::Mutex::new(()))
|
||||
.lock()
|
||||
.map_err(|_| "stderr suppression lock poisoned".to_string())?;
|
||||
let _guard = SuppressStderr::new();
|
||||
let mut clipboard =
|
||||
arboard::Clipboard::new().map_err(|e| format!("clipboard unavailable: {e}"))?;
|
||||
clipboard
|
||||
.set_text(text)
|
||||
.map_err(|e| format!("failed to set clipboard text: {e}"))?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn arboard_copy(text: &str) -> Result<Option<ClipboardLease>, String> {
|
||||
let _guard = SuppressStderr::new();
|
||||
let mut clipboard =
|
||||
arboard::Clipboard::new().map_err(|e| format!("clipboard unavailable: {e}"))?;
|
||||
clipboard
|
||||
.set_text(text)
|
||||
.map_err(|e| format!("failed to set clipboard text: {e}"))?;
|
||||
Ok(Some(ClipboardLease::native_linux(clipboard)))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn arboard_copy(_text: &str) -> Result<Option<ClipboardLease>, String> {
|
||||
Err("native clipboard unavailable on Android".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn wsl_clipboard_copy(text: &str) -> Result<(), String> {
|
||||
let mut child = std::process::Command::new("powershell.exe")
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; $ErrorActionPreference = 'Stop'; $text = [Console]::In.ReadToEnd(); Set-Clipboard -Value $text",
|
||||
])
|
||||
.spawn()
|
||||
.map_err(|e| format!("failed to spawn powershell.exe: {e}"))?;
|
||||
|
||||
let Some(mut stdin) = child.stdin.take() else {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err("failed to open powershell.exe stdin".to_string());
|
||||
};
|
||||
|
||||
if let Err(err) = stdin.write_all(text.as_bytes()) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(format!("failed to write to powershell.exe: {err}"));
|
||||
}
|
||||
|
||||
drop(stdin);
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.map_err(|e| format!("failed to wait for powershell.exe: {e}"))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
if stderr.is_empty() {
|
||||
let status = output.status;
|
||||
Err(format!("powershell.exe exited with status {status}"))
|
||||
} else {
|
||||
Err(format!("powershell.exe failed: {stderr}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn wsl_clipboard_copy(_text: &str) -> Result<(), String> {
|
||||
Err("WSL clipboard fallback unavailable on this platform".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct SuppressStderr {
|
||||
saved_fd: Option<libc::c_int>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl SuppressStderr {
|
||||
fn new() -> Self {
|
||||
unsafe {
|
||||
let saved = libc::dup(2);
|
||||
if saved < 0 {
|
||||
return Self { saved_fd: None };
|
||||
}
|
||||
let devnull = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
|
||||
if devnull < 0 {
|
||||
libc::close(saved);
|
||||
return Self { saved_fd: None };
|
||||
}
|
||||
if libc::dup2(devnull, 2) < 0 {
|
||||
libc::close(saved);
|
||||
libc::close(devnull);
|
||||
return Self { saved_fd: None };
|
||||
}
|
||||
libc::close(devnull);
|
||||
Self {
|
||||
saved_fd: Some(saved),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for SuppressStderr {
|
||||
fn drop(&mut self) {
|
||||
if let Some(saved) = self.saved_fd {
|
||||
unsafe {
|
||||
libc::dup2(saved, 2);
|
||||
libc::close(saved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
struct SuppressStderr;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
impl SuppressStderr {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
fn osc52_copy(text: &str) -> Result<(), String> {
|
||||
let sequence = osc52_sequence(text, std::env::var_os("TMUX").is_some())?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
match std::fs::OpenOptions::new().write(true).open("/dev/tty") {
|
||||
Ok(tty) => match write_osc52_to_writer(tty, &sequence) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(err) => tracing::debug!(
|
||||
"failed to write OSC 52 to /dev/tty: {err}; falling back to stdout"
|
||||
),
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::debug!("failed to open /dev/tty for OSC 52: {err}; falling back to stdout")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_osc52_to_writer(std::io::stdout().lock(), &sequence)
|
||||
}
|
||||
|
||||
fn write_osc52_to_writer(mut writer: impl Write, sequence: &str) -> Result<(), String> {
|
||||
writer
|
||||
.write_all(sequence.as_bytes())
|
||||
.map_err(|e| format!("failed to write OSC 52: {e}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| format!("failed to flush OSC 52: {e}"))
|
||||
}
|
||||
|
||||
fn osc52_sequence(text: &str, tmux: bool) -> Result<String, String> {
|
||||
let raw_bytes = text.len();
|
||||
if raw_bytes > OSC52_MAX_RAW_BYTES {
|
||||
return Err(format!(
|
||||
"OSC 52 payload too large ({raw_bytes} bytes; max {OSC52_MAX_RAW_BYTES})"
|
||||
));
|
||||
}
|
||||
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(text.as_bytes());
|
||||
if tmux {
|
||||
Ok(format!("\x1bPtmux;\x1b\x1b]52;c;{encoded}\x07\x1b\\"))
|
||||
} else {
|
||||
Ok(format!("\x1b]52;c;{encoded}\x07"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::OSC52_MAX_RAW_BYTES;
|
||||
use super::osc52_sequence;
|
||||
use super::write_osc52_to_writer;
|
||||
|
||||
#[test]
|
||||
fn osc52_encoding_roundtrips() {
|
||||
use base64::Engine;
|
||||
let text = "# Hello\n\n```rust\nfn main() {}\n```\n";
|
||||
let sequence = osc52_sequence(text, false).expect("OSC 52 sequence");
|
||||
let encoded = sequence
|
||||
.trim_start_matches("\u{1b}]52;c;")
|
||||
.trim_end_matches('\u{7}');
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.unwrap();
|
||||
assert_eq!(decoded, text.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc52_rejects_payload_larger_than_limit() {
|
||||
let text = "x".repeat(OSC52_MAX_RAW_BYTES + 1);
|
||||
assert_eq!(
|
||||
osc52_sequence(&text, false),
|
||||
Err(format!(
|
||||
"OSC 52 payload too large ({} bytes; max {OSC52_MAX_RAW_BYTES})",
|
||||
OSC52_MAX_RAW_BYTES + 1
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_osc52_to_writer_emits_sequence_verbatim() {
|
||||
let sequence = "\u{1b}]52;c;aGVsbG8=\u{7}";
|
||||
let mut output = Vec::new();
|
||||
assert_eq!(write_osc52_to_writer(&mut output, sequence), Ok(()));
|
||||
assert_eq!(output, sequence.as_bytes());
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,7 @@ mod foundation;
|
||||
mod ui_consts;
|
||||
mod user_approval_widget;
|
||||
mod height_manager;
|
||||
mod clipboard_copy;
|
||||
mod clipboard_paste;
|
||||
mod greeting;
|
||||
// Upstream introduced a standalone status indicator widget. Our fork renders
|
||||
|
||||
@@ -53,11 +53,13 @@ pub enum SlashCommand {
|
||||
Browser,
|
||||
Chrome,
|
||||
New,
|
||||
Clear,
|
||||
Init,
|
||||
Compact,
|
||||
Undo,
|
||||
Review,
|
||||
Cloud,
|
||||
Copy,
|
||||
Diff,
|
||||
Mention,
|
||||
Cmd,
|
||||
@@ -92,6 +94,7 @@ pub enum SlashCommand {
|
||||
Code,
|
||||
Logout,
|
||||
Quit,
|
||||
Exit,
|
||||
#[cfg(debug_assertions)]
|
||||
TestApproval,
|
||||
}
|
||||
@@ -115,7 +118,9 @@ impl SlashCommand {
|
||||
SlashCommand::Undo => "restore the workspace to the last Code snapshot",
|
||||
SlashCommand::Review => "review your changes for potential issues",
|
||||
SlashCommand::Cloud => "browse, apply, and create cloud tasks",
|
||||
SlashCommand::Quit => "exit Code",
|
||||
SlashCommand::Quit | SlashCommand::Exit => "exit Code",
|
||||
SlashCommand::Clear => "clear the terminal and start a new chat",
|
||||
SlashCommand::Copy => "copy last response as markdown",
|
||||
SlashCommand::Diff => "show git diff (including untracked files)",
|
||||
SlashCommand::Mention => "mention a file",
|
||||
SlashCommand::Cmd => "run a project command",
|
||||
@@ -237,7 +242,7 @@ pub fn process_slash_command_message(message: &str) -> ProcessedCommand {
|
||||
let args_raw = parts.next().map(|s| s.trim()).unwrap_or("");
|
||||
let canonical_command = command_str.to_ascii_lowercase();
|
||||
|
||||
if matches!(canonical_command.as_str(), "quit" | "exit") {
|
||||
if !has_slash && matches!(canonical_command.as_str(), "quit" | "exit") {
|
||||
if !has_slash && !args_raw.is_empty() {
|
||||
return ProcessedCommand::NotCommand(message.to_string());
|
||||
}
|
||||
@@ -347,4 +352,21 @@ mod tests {
|
||||
other => panic!("expected RegularCommand, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_compat_commands_are_regular_commands() {
|
||||
for (input, expected) in [
|
||||
("/exit", SlashCommand::Exit),
|
||||
("/clear", SlashCommand::Clear),
|
||||
("/copy", SlashCommand::Copy),
|
||||
] {
|
||||
match process_slash_command_message(input) {
|
||||
ProcessedCommand::RegularCommand(command, command_text) => {
|
||||
assert_eq!(command, expected);
|
||||
assert_eq!(command_text, input);
|
||||
}
|
||||
other => panic!("expected RegularCommand for {input}, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ Notes
|
||||
- `/browser`: open internal browser.
|
||||
- `/chrome`: connect to your Chrome browser.
|
||||
- `/new`: start a new chat during a conversation.
|
||||
- `/clear`: clear the terminal and start a new chat.
|
||||
- `/resume`: resume a past session for this folder.
|
||||
- `/rename <name>`: rename the current session (shown in the resume list).
|
||||
- `/quit`: exit Code.
|
||||
- `/exit`: exit Code.
|
||||
- `/logout`: log out of Code.
|
||||
- `/login`: manage Code sign-ins (select, add, or disconnect accounts).
|
||||
- `/settings [section]`: open the settings panel. Optional section argument
|
||||
@@ -29,6 +31,7 @@ Notes
|
||||
|
||||
- `/init`: create an `AGENTS.md` file with instructions for Code.
|
||||
- `/diff`: show `git diff` (including untracked files).
|
||||
- `/copy`: copy the last assistant response as markdown.
|
||||
- `/undo`: open a snapshot picker so you can restore workspace files to a
|
||||
previous Code snapshot and optionally rewind the conversation to that point.
|
||||
- `/branch [task]`: create a worktree branch and switch to it. If a
|
||||
|
||||
Reference in New Issue
Block a user