mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
feat(tui/notifications): add OSC toggle and slash command
This commit is contained in:
@@ -855,6 +855,42 @@ pub fn set_tui_alternate_screen(codex_home: &Path, enabled: bool) -> anyhow::Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist the TUI notifications preference into `CODEX_HOME/config.toml` at `[tui].notifications`.
|
||||
pub fn set_tui_notifications(
|
||||
codex_home: &Path,
|
||||
notifications: crate::config_types::Notifications,
|
||||
) -> anyhow::Result<()> {
|
||||
use crate::config_types::Notifications;
|
||||
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
|
||||
let mut doc = match std::fs::read_to_string(&read_path) {
|
||||
Ok(contents) => contents.parse::<DocumentMut>()?,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
match notifications {
|
||||
Notifications::Enabled(value) => {
|
||||
doc["tui"]["notifications"] = toml_edit::value(value);
|
||||
}
|
||||
Notifications::Custom(values) => {
|
||||
let mut array = TomlArray::default();
|
||||
for value in values {
|
||||
array.push(value);
|
||||
}
|
||||
doc["tui"]["notifications"] = TomlItem::Value(array.into());
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(codex_home)?;
|
||||
let tmp_file = NamedTempFile::new_in(codex_home)?;
|
||||
std::fs::write(tmp_file.path(), doc.to_string())?;
|
||||
tmp_file.persist(config_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist the GitHub workflow check preference under `[github].check_workflows_on_push`.
|
||||
pub fn set_github_check_on_push(codex_home: &Path, enabled: bool) -> anyhow::Result<()> {
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
|
||||
@@ -418,6 +418,66 @@ impl App<'_> {
|
||||
);
|
||||
}
|
||||
|
||||
fn sanitize_notification_text(input: &str) -> String {
|
||||
let mut sanitized = String::with_capacity(input.len());
|
||||
for ch in input.chars() {
|
||||
match ch {
|
||||
'\u{00}'..='\u{08}' | '\u{0B}' | '\u{0C}' | '\u{0E}'..='\u{1F}' | '\u{7F}' => {}
|
||||
'\n' | '\r' | '\t' => {
|
||||
if !sanitized.ends_with(' ') {
|
||||
sanitized.push(' ');
|
||||
}
|
||||
}
|
||||
_ => sanitized.push(ch),
|
||||
}
|
||||
}
|
||||
sanitized
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn format_notification_message(title: &str, body: Option<&str>) -> Option<String> {
|
||||
let title = Self::sanitize_notification_text(title);
|
||||
let body = body.map(Self::sanitize_notification_text);
|
||||
let mut message = match body {
|
||||
Some(ref b) if !b.is_empty() => {
|
||||
if title.is_empty() {
|
||||
b.clone()
|
||||
} else {
|
||||
format!("{}: {}", title, b)
|
||||
}
|
||||
}
|
||||
_ => title.clone(),
|
||||
};
|
||||
|
||||
if message.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
const MAX_LEN: usize = 160;
|
||||
if message.chars().count() > MAX_LEN {
|
||||
let mut truncated = String::new();
|
||||
for ch in message.chars() {
|
||||
if truncated.chars().count() >= MAX_LEN.saturating_sub(3) {
|
||||
break;
|
||||
}
|
||||
truncated.push(ch);
|
||||
}
|
||||
truncated.push_str("...");
|
||||
message = truncated;
|
||||
}
|
||||
|
||||
Some(message)
|
||||
}
|
||||
|
||||
fn emit_osc9_notification(message: &str) {
|
||||
let payload = format!("\u{1b}]9;{}\u{7}", message);
|
||||
let mut stdout = std::io::stdout();
|
||||
let _ = stdout.write_all(payload.as_bytes());
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
|
||||
|
||||
/// Schedule a redraw immediately and open a short debounce window to coalesce
|
||||
/// subsequent requests. Crucially, even if a timer is already armed (e.g., an
|
||||
@@ -1705,6 +1765,11 @@ impl App<'_> {
|
||||
widget.handle_update_command();
|
||||
}
|
||||
}
|
||||
SlashCommand::Notifications => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_notifications_command();
|
||||
}
|
||||
}
|
||||
SlashCommand::Agents => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_agents_command(command_args);
|
||||
@@ -1884,6 +1949,11 @@ impl App<'_> {
|
||||
widget.set_github_watcher(enabled);
|
||||
}
|
||||
}
|
||||
AppEvent::UpdateTuiNotifications(enabled) => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.set_tui_notifications(enabled);
|
||||
}
|
||||
}
|
||||
AppEvent::UpdateValidationTool { name, enable } => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.toggle_validation_tool(&name, enable);
|
||||
@@ -1898,6 +1968,11 @@ impl App<'_> {
|
||||
self.terminal_title_override = title;
|
||||
self.apply_terminal_title();
|
||||
}
|
||||
AppEvent::EmitTuiNotification { title, body } => {
|
||||
if let Some(message) = Self::format_notification_message(&title, body.as_deref()) {
|
||||
Self::emit_osc9_notification(&message);
|
||||
}
|
||||
}
|
||||
AppEvent::UpdateMcpServer { name, enable } => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.toggle_mcp_server(&name, enable);
|
||||
|
||||
@@ -85,6 +85,9 @@ pub(crate) enum AppEvent {
|
||||
/// Update the terminal title override. `None` restores the default title.
|
||||
SetTerminalTitle { title: Option<String> },
|
||||
|
||||
/// Emit a best-effort OSC 9 notification from the terminal.
|
||||
EmitTuiNotification { title: String, body: Option<String> },
|
||||
|
||||
/// Schedule a one-shot animation frame roughly after the given duration.
|
||||
/// Multiple requests are coalesced by the central frame scheduler.
|
||||
ScheduleFrameIn(Duration),
|
||||
@@ -140,6 +143,8 @@ pub(crate) enum AppEvent {
|
||||
|
||||
/// Update GitHub workflow monitoring toggle
|
||||
UpdateGithubWatcher(bool),
|
||||
/// Update the TUI notifications toggle
|
||||
UpdateTuiNotifications(bool),
|
||||
/// Enable/disable a specific validation tool
|
||||
UpdateValidationTool { name: String, enable: bool },
|
||||
/// Enable/disable an entire validation group
|
||||
|
||||
@@ -41,6 +41,7 @@ impl AppEventSender {
|
||||
| AppEvent::Redraw
|
||||
| AppEvent::ExitRequest
|
||||
| AppEvent::SetTerminalTitle { .. }
|
||||
| AppEvent::EmitTuiNotification { .. }
|
||||
);
|
||||
|
||||
let tx = if is_high { &self.high_tx } else { &self.bulk_tx };
|
||||
|
||||
@@ -46,6 +46,7 @@ mod verbosity_selection_view;
|
||||
pub(crate) mod validation_settings_view;
|
||||
mod update_settings_view;
|
||||
mod undo_restore_view;
|
||||
mod notifications_settings_view;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CancellationEvent {
|
||||
@@ -63,6 +64,7 @@ pub(crate) use login_accounts_view::{
|
||||
};
|
||||
|
||||
pub(crate) use update_settings_view::{UpdateSettingsView, UpdateSharedState};
|
||||
pub(crate) use notifications_settings_view::{NotificationsMode, NotificationsSettingsView};
|
||||
|
||||
use codex_core::protocol::Op;
|
||||
use approval_modal_view::ApprovalModalView;
|
||||
@@ -161,6 +163,12 @@ impl BottomPane<'_> {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn show_notifications_settings(&mut self, view: NotificationsSettingsView) {
|
||||
self.active_view = Some(Box::new(view));
|
||||
self.status_view_active = false;
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Show per-agent editor
|
||||
pub fn show_agent_editor(
|
||||
&mut self,
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget};
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::BottomPane;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum NotificationsMode {
|
||||
Toggle { enabled: bool },
|
||||
Custom { command: Vec<String> },
|
||||
}
|
||||
|
||||
pub(crate) struct NotificationsSettingsView {
|
||||
mode: NotificationsMode,
|
||||
app_event_tx: AppEventSender,
|
||||
selected_row: usize,
|
||||
is_complete: bool,
|
||||
}
|
||||
|
||||
impl NotificationsSettingsView {
|
||||
pub fn new(mode: NotificationsMode, app_event_tx: AppEventSender) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
app_event_tx,
|
||||
selected_row: 0,
|
||||
is_complete: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(&mut self) {
|
||||
match &mut self.mode {
|
||||
NotificationsMode::Toggle { enabled } => {
|
||||
*enabled = !*enabled;
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateTuiNotifications(*enabled));
|
||||
}
|
||||
NotificationsMode::Custom { .. } => {
|
||||
self.app_event_tx.send_background_event(
|
||||
"TUI notifications use a custom command. Edit ~/.code/config.toml to change this.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn status_line(&self) -> Line<'static> {
|
||||
match &self.mode {
|
||||
NotificationsMode::Toggle { enabled } => {
|
||||
let status = if *enabled { "Enabled" } else { "Disabled" };
|
||||
let color = if *enabled {
|
||||
crate::colors::success()
|
||||
} else {
|
||||
crate::colors::warning()
|
||||
};
|
||||
Line::from(vec![
|
||||
Span::styled("Status: ", Style::default().fg(crate::colors::text_dim())),
|
||||
Span::styled(status, Style::default().fg(color).add_modifier(Modifier::BOLD)),
|
||||
])
|
||||
}
|
||||
NotificationsMode::Custom { command } => {
|
||||
let command_display = if command.is_empty() {
|
||||
"<empty>".to_string()
|
||||
} else {
|
||||
command.join(" ")
|
||||
};
|
||||
Line::from(vec![
|
||||
Span::styled("Status: ", Style::default().fg(crate::colors::text_dim())),
|
||||
Span::styled("Custom command", Style::default().fg(crate::colors::info()).add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" "),
|
||||
Span::styled(command_display, Style::default().fg(crate::colors::dim())),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_line(&self) -> Line<'static> {
|
||||
match &self.mode {
|
||||
NotificationsMode::Toggle { enabled } => {
|
||||
let label = if *enabled { "Enabled" } else { "Disabled" };
|
||||
Line::from(vec![
|
||||
Span::styled("Notifications: ", Style::default().fg(crate::colors::text_dim())),
|
||||
Span::raw(label),
|
||||
])
|
||||
}
|
||||
NotificationsMode::Custom { .. } => {
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
"Notifications are managed by your config file.",
|
||||
Style::default().fg(crate::colors::text()),
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BottomPaneView<'a> for NotificationsSettingsView {
|
||||
fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, key_event: KeyEvent) {
|
||||
match key_event {
|
||||
KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::NONE, .. } => {
|
||||
if self.selected_row > 0 {
|
||||
self.selected_row -= 1;
|
||||
}
|
||||
}
|
||||
KeyEvent { code: KeyCode::Down, modifiers: KeyModifiers::NONE, .. } => {
|
||||
if self.selected_row < 1 {
|
||||
self.selected_row += 1;
|
||||
}
|
||||
}
|
||||
KeyEvent { code: KeyCode::Left | KeyCode::Right, modifiers: KeyModifiers::NONE, .. } => {
|
||||
if self.selected_row == 0 {
|
||||
self.toggle();
|
||||
}
|
||||
}
|
||||
KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. } => {
|
||||
if self.selected_row == 0 {
|
||||
self.toggle();
|
||||
} else {
|
||||
self.is_complete = true;
|
||||
}
|
||||
}
|
||||
KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::NONE, .. } => {
|
||||
if self.selected_row == 0 {
|
||||
self.toggle();
|
||||
}
|
||||
}
|
||||
KeyEvent { code: KeyCode::Esc, .. } => {
|
||||
self.is_complete = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
self.is_complete
|
||||
}
|
||||
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
9
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
Clear.render(area, buf);
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(crate::colors::border()))
|
||||
.style(Style::default().bg(crate::colors::background()).fg(crate::colors::text()))
|
||||
.title(" Notifications ")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
lines.push(self.status_line());
|
||||
lines.push(Line::from(""));
|
||||
let mut toggle_line = self.toggle_line();
|
||||
if self.selected_row == 0 {
|
||||
toggle_line = toggle_line
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(crate::colors::selection())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
lines.push(toggle_line);
|
||||
lines.push(Line::from(""));
|
||||
let mut close_line = Line::from(vec![
|
||||
Span::raw(if self.selected_row == 1 { "> " } else { " " }),
|
||||
Span::raw("Close"),
|
||||
]);
|
||||
if self.selected_row == 1 {
|
||||
close_line = close_line
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(crate::colors::selection())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
lines.push(close_line);
|
||||
lines.push(Line::from(""));
|
||||
|
||||
let footer = match &self.mode {
|
||||
NotificationsMode::Toggle { .. } => Line::from(vec![
|
||||
Span::styled("Up/Down", Style::default().fg(crate::colors::light_blue())),
|
||||
Span::raw(" Navigate "),
|
||||
Span::styled("Left/Right or Space", Style::default().fg(crate::colors::success())),
|
||||
Span::raw(" Toggle "),
|
||||
Span::styled("Enter", Style::default().fg(crate::colors::success())),
|
||||
Span::raw(" Toggle or Close "),
|
||||
Span::styled("Esc", Style::default().fg(crate::colors::error())),
|
||||
Span::raw(" Cancel"),
|
||||
]),
|
||||
NotificationsMode::Custom { .. } => Line::from(vec![
|
||||
Span::styled("Edit ", Style::default().fg(crate::colors::text_dim())),
|
||||
Span::styled("[tui].notifications", Style::default().fg(crate::colors::info())),
|
||||
Span::styled(" in ~/.code/config.toml to change this setting.", Style::default().fg(crate::colors::text_dim())),
|
||||
]),
|
||||
};
|
||||
lines.push(footer);
|
||||
|
||||
let paragraph = Paragraph::new(lines)
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().bg(crate::colors::background()).fg(crate::colors::text()));
|
||||
paragraph.render(
|
||||
Rect {
|
||||
x: inner.x.saturating_add(1),
|
||||
y: inner.y,
|
||||
width: inner.width.saturating_sub(2),
|
||||
height: inner.height,
|
||||
},
|
||||
buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::git_info::CommitLogEntry;
|
||||
use codex_core::config_types::AgentConfig;
|
||||
use codex_core::config_types::Notifications;
|
||||
use codex_core::config_types::ReasoningEffort;
|
||||
use codex_core::config_types::TextVerbosity;
|
||||
use codex_core::plan_tool::{PlanItemArg, StepStatus, UpdatePlanArgs};
|
||||
@@ -5828,9 +5829,7 @@ impl ChatWidget<'_> {
|
||||
|
||||
self.mark_needs_redraw();
|
||||
}
|
||||
EventMsg::TaskComplete(TaskCompleteEvent {
|
||||
last_agent_message: _,
|
||||
}) => {
|
||||
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
|
||||
// Finalize any active streams
|
||||
if self.stream.is_write_cycle_active() {
|
||||
// Finalize both streams via streaming facade
|
||||
@@ -5911,6 +5910,7 @@ impl ChatWidget<'_> {
|
||||
self.stream_state.current_kind = None;
|
||||
// Final re-check for idle state
|
||||
self.maybe_hide_spinner();
|
||||
self.emit_turn_complete_notification(last_agent_message);
|
||||
self.mark_needs_redraw();
|
||||
}
|
||||
EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent {
|
||||
@@ -7434,6 +7434,18 @@ impl ChatWidget<'_> {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn handle_notifications_command(&mut self) {
|
||||
use crate::bottom_pane::{NotificationsMode, NotificationsSettingsView};
|
||||
|
||||
let mode = match &self.config.tui.notifications {
|
||||
Notifications::Enabled(enabled) => NotificationsMode::Toggle { enabled: *enabled },
|
||||
Notifications::Custom(command) => NotificationsMode::Custom { command: command.clone() },
|
||||
};
|
||||
|
||||
let view = NotificationsSettingsView::new(mode, self.app_event_tx.clone());
|
||||
self.bottom_pane.show_notifications_settings(view);
|
||||
}
|
||||
|
||||
pub(crate) fn add_prompts_output(&mut self) {
|
||||
self.history_push(history_cell::new_prompts_output());
|
||||
}
|
||||
@@ -19350,6 +19362,80 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_tui_notifications(&mut self, enabled: bool) {
|
||||
let new_state = Notifications::Enabled(enabled);
|
||||
self.config.tui.notifications = new_state.clone();
|
||||
self.config.tui_notifications = new_state.clone();
|
||||
|
||||
match find_codex_home() {
|
||||
Ok(home) => {
|
||||
match codex_core::config::set_tui_notifications(&home, new_state) {
|
||||
Ok(()) => {
|
||||
let msg = format!(
|
||||
"✅ {} TUI notifications",
|
||||
if enabled { "Enabled" } else { "Disabled" }
|
||||
);
|
||||
self.push_background_tail(msg);
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = format!(
|
||||
"⚠️ Failed to persist TUI notifications setting: {}",
|
||||
err
|
||||
);
|
||||
self.history_push(history_cell::new_error_event(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let msg = format!(
|
||||
"✅ {} TUI notifications (not persisted: CODE_HOME/CODEX_HOME not found)",
|
||||
if enabled { "Enabled" } else { "Disabled" }
|
||||
);
|
||||
self.push_background_tail(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_turn_complete_notification(&self, last_agent_message: Option<String>) {
|
||||
if !matches!(self.config.tui.notifications, Notifications::Enabled(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let snippet = last_agent_message
|
||||
.as_deref()
|
||||
.map(Self::notification_snippet)
|
||||
.filter(|text| !text.is_empty());
|
||||
|
||||
self.app_event_tx.send(AppEvent::EmitTuiNotification {
|
||||
title: "Code".to_string(),
|
||||
body: snippet,
|
||||
});
|
||||
}
|
||||
|
||||
fn notification_snippet(input: &str) -> String {
|
||||
let collapsed = input
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
const LIMIT: usize = 120;
|
||||
if collapsed.chars().count() <= LIMIT {
|
||||
return collapsed;
|
||||
}
|
||||
|
||||
let mut truncated = String::new();
|
||||
let mut count = 0usize;
|
||||
for ch in collapsed.chars() {
|
||||
if count >= LIMIT.saturating_sub(3) {
|
||||
break;
|
||||
}
|
||||
truncated.push(ch);
|
||||
count += 1;
|
||||
}
|
||||
truncated.push_str("...");
|
||||
truncated
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_mcp_server(&mut self, name: &str, enable: bool) {
|
||||
match codex_core::config::find_codex_home() {
|
||||
Ok(home) => match codex_core::config::set_mcp_server_enabled(&home, name, enable) {
|
||||
|
||||
@@ -64,6 +64,7 @@ pub enum SlashCommand {
|
||||
Limits,
|
||||
#[strum(serialize = "update", serialize = "upgrade")]
|
||||
Update,
|
||||
Notifications,
|
||||
Theme,
|
||||
Model,
|
||||
Reasoning,
|
||||
@@ -113,6 +114,7 @@ impl SlashCommand {
|
||||
SlashCommand::Status => "show current session configuration and token usage",
|
||||
SlashCommand::Limits => "visualize weekly and hourly rate limits",
|
||||
SlashCommand::Update => "check for updates and optionally upgrade",
|
||||
SlashCommand::Notifications => "toggle TUI notifications (status/on/off)",
|
||||
SlashCommand::Theme => "switch between color themes",
|
||||
SlashCommand::Prompts => "show example prompts",
|
||||
SlashCommand::Model => "choose model & reasoning effort",
|
||||
|
||||
@@ -49,6 +49,7 @@ Notes
|
||||
- `/limits`: visualize current hourly and weekly rate-limit usage.
|
||||
- `/update`: check the installed version, detect available upgrades, and run the
|
||||
appropriate installer when possible.
|
||||
- `/notifications`: inspect and toggle TUI notifications.
|
||||
- `/mcp`: manage MCP servers (status/on/off/add).
|
||||
- `/validation [status|on|off|<tool> (on|off)]`: inspect or toggle validation
|
||||
harness settings.
|
||||
|
||||
Reference in New Issue
Block a user