Synced from monorepo

Changes:
- Gate session-lifecycle heap steady state with a dhat soak
- Unbreak merge lifecycle e2e after default model → grok-4.5
- Scan home-scope rules dirs at <root>/rules
- Complete text-input paste and terminal parity
- Gate project roles and personas
- Use canonical editing in dialogs
- Use canonical editing in search bars
- Reject ambiguous MCP tool IDs
- Harden Git operands for plugins
- Simplify queue drain API
- Pass RFC 9207 iss through MCP OAuth token exchange
- Show leader roster when local agents map is empty
- Use canonical editing in Persona views
- Remove marketplace default-skills auto-install and purge old installs
- Use canonical editing in extension forms
- Add canonical dashboard text editing
- Use canonical editing in settings
- Add /summarize as a /recap alias
- Restore previous agent when exiting dashboard
- Use tool_choice auto for compaction
- Settings toggle for snap-prompt-to-top on send
- Update default models to grok-4.5
- Source login shell once for local bash (env + alias/function snapshot)
- Template hardcoded param names in server-native tool descriptions
- Fix System-Reminder XML tag injection in CLAUDE.md via agents_md
- Fix remote workspace-server hardcoding LSP trust (repo code execution risk)
- Clear orphaned tool-call updates at turn end
- Suppress task wake after cancel
- Send x-grok-client-identifier on direct API tool calls
- Harden dashboard peek lease transitions
- Host /btw side panel in live region (minimal mode)
- Bound scroll presentation latency
- Highlight multi-line constructs correctly in diffs and the file viewer
- Block web_fetch non-public IPs; local opt-in is explicit-host only
- Seed coding_data_retention_opt_out=false for OAuth e2es in pty-harness
- Follow up clipboard delivery feedback
- Use canonical editing in pickers
- Route TextArea through canonical editor
- Persistent "watching" status row; quieter turn markers
- Gate sensitive edit targets
- Expose agent registry counts and gate session churn on them
- Default coding data sharing to opt-out until server preference applies
- Wire chat attachment ids through gateway prompts
- On auth refresh failure, issue retry
- Forward preview provenance and computer lifecycle state
- Document independent privacy controls and scope /privacy output
- Strip SamplingError Display prefix on rate-limit UI copy
- Stop dumping Cloudflare HTML into Retry failed
- Disable in-place prompt edit (scroll jank on enter)
- Strip forced ANSI color from gh pr view JSON
- Plumb bash tool description onto ToolUsageCard wire
This commit is contained in:
grokkybara[bot] 2026-07-18 19:48:28 +01:00
commit 7cfcb20d2b
292 changed files with 23315 additions and 9209 deletions

View file

@ -0,0 +1,304 @@
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use xai_ratatui_textarea::{
EditBuffer, EditCommand, EditOutcome, SingleLineViewport, classify_key_event,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LineEditOutcome {
Unhandled,
/// The key was recognized and consumed, but text and cursor stayed unchanged.
HandledNoChange,
CursorChanged,
TextChanged,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct LineEditor {
buffer: EditBuffer,
}
impl LineEditor {
pub(crate) fn text(&self) -> &str {
self.buffer.text()
}
pub(crate) fn cursor_byte(&self) -> usize {
self.buffer.cursor_byte()
}
pub(crate) fn set_text(&mut self, text: impl Into<String>) {
self.buffer = EditBuffer::from_text(sanitize_single_line(text));
}
pub(crate) fn reset(&mut self) {
self.buffer = EditBuffer::new();
}
#[cfg(test)]
pub(crate) fn set_cursor_byte(&mut self, cursor_byte: usize) -> LineEditOutcome {
Self::from_edit_outcome(self.buffer.set_cursor_byte(cursor_byte))
}
pub(crate) fn delete_last_grapheme(&mut self) -> LineEditOutcome {
let _ = self.buffer.set_cursor_byte(self.buffer.text().len());
Self::from_edit_outcome(self.buffer.apply(EditCommand::DeleteGraphemeBackward))
}
pub(crate) fn insert_paste(&mut self, text: &str) -> LineEditOutcome {
self.insert_paste_with_policy(text, |_| true, usize::MAX)
}
pub(crate) fn insert_paste_with_policy(
&mut self,
text: &str,
mut allow_insert: impl FnMut(char) -> bool,
max_chars: usize,
) -> LineEditOutcome {
let cleaned = sanitize_single_line(text);
let accepted = cleaned
.chars()
.filter(|character| allow_insert(*character))
.take(max_chars)
.collect::<String>();
if accepted.is_empty() {
return LineEditOutcome::HandledNoChange;
}
Self::from_edit_outcome(self.buffer.insert_str(&accepted))
}
pub(crate) fn insert_paste_with_byte_limit(
&mut self,
text: &str,
max_total_bytes: usize,
) -> LineEditOutcome {
let cleaned = sanitize_single_line(text);
let remaining = max_total_bytes.saturating_sub(self.buffer.text().len());
let mut accepted_bytes = 0usize;
let accepted = cleaned
.chars()
.take_while(|character| {
let next = accepted_bytes + character.len_utf8();
if next > remaining {
return false;
}
accepted_bytes = next;
true
})
.collect::<String>();
if accepted.is_empty() {
return LineEditOutcome::HandledNoChange;
}
Self::from_edit_outcome(self.buffer.insert_str(&accepted))
}
pub(crate) fn handle_key(&mut self, key: &KeyEvent) -> LineEditOutcome {
self.handle_key_with_insert_policy(key, |_| true)
}
pub(crate) fn handle_key_with_insert_policy(
&mut self,
key: &KeyEvent,
allow_insert: impl FnOnce(char) -> bool,
) -> LineEditOutcome {
if key.kind == KeyEventKind::Release {
return LineEditOutcome::Unhandled;
}
let command = match key {
KeyEvent {
code: KeyCode::Home,
..
}
| KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::SUPER,
..
} => Some(EditCommand::MoveLogicalLineStart),
KeyEvent {
code: KeyCode::End, ..
}
| KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::SUPER,
..
} => Some(EditCommand::MoveLogicalLineEnd),
_ => classify_key_event(key),
};
let Some(command) = command else {
return LineEditOutcome::Unhandled;
};
if let EditCommand::Insert(character) = command
&& !allow_insert(character)
{
return LineEditOutcome::HandledNoChange;
}
Self::from_edit_outcome(self.buffer.apply(command))
}
pub(crate) fn viewport(&self, width: usize) -> SingleLineViewport {
self.buffer.single_line_viewport(width)
}
fn from_edit_outcome(outcome: EditOutcome) -> LineEditOutcome {
match outcome {
EditOutcome::Unchanged => LineEditOutcome::HandledNoChange,
EditOutcome::CursorOnly => LineEditOutcome::CursorChanged,
EditOutcome::TextOnly(_) | EditOutcome::TextAndCursor(_) => {
LineEditOutcome::TextChanged
}
}
}
}
pub(crate) fn sanitize_single_line(text: impl Into<String>) -> String {
let mut text = text.into();
text.retain(|character| !matches!(character, '\r' | '\n'));
text
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, modifiers)
}
#[test]
fn canonical_word_keys_and_legacy_alt_bindings() {
for event in [
key(KeyCode::Left, KeyModifiers::ALT),
key(KeyCode::Char('b'), KeyModifiers::ALT),
key(KeyCode::Left, KeyModifiers::CONTROL),
] {
let mut editor = LineEditor::default();
editor.set_text("hello-world");
assert_eq!(editor.handle_key(&event), LineEditOutcome::CursorChanged);
assert_eq!(editor.cursor_byte(), "hello-".len());
}
let mut editor = LineEditor::default();
editor.set_text("hello-world");
assert_eq!(
editor.handle_key(&key(KeyCode::Backspace, KeyModifiers::ALT)),
LineEditOutcome::TextChanged
);
assert_eq!(editor.text(), "hello-");
}
#[test]
fn home_end_super_and_outcomes() {
let mut editor = LineEditor::default();
editor.set_text("abc");
assert_eq!(
editor.handle_key(&key(KeyCode::Home, KeyModifiers::NONE)),
LineEditOutcome::CursorChanged
);
assert_eq!(
editor.handle_key(&key(KeyCode::Left, KeyModifiers::SUPER)),
LineEditOutcome::HandledNoChange
);
assert_eq!(
editor.handle_key(&key(KeyCode::Right, KeyModifiers::SUPER)),
LineEditOutcome::CursorChanged
);
assert_eq!(
editor.handle_key(&key(KeyCode::End, KeyModifiers::NONE)),
LineEditOutcome::HandledNoChange
);
assert_eq!(
editor.handle_key(&key(KeyCode::Esc, KeyModifiers::NONE)),
LineEditOutcome::Unhandled
);
}
#[test]
fn ctrl_u_kills_only_to_the_cursor() {
let mut editor = LineEditor::default();
editor.set_text("hello world");
assert_eq!(
editor.handle_key(&key(KeyCode::Left, KeyModifiers::NONE)),
LineEditOutcome::CursorChanged
);
assert_eq!(
editor.handle_key(&key(KeyCode::Char('u'), KeyModifiers::CONTROL)),
LineEditOutcome::TextChanged
);
assert_eq!(editor.text(), "d");
assert_eq!(editor.cursor_byte(), 0);
}
#[test]
fn paste_byte_limit_keeps_whole_characters_at_cursor() {
let mut editor = LineEditor::default();
editor.set_text("ab");
let _ = editor.set_cursor_byte(1);
assert_eq!(
editor.insert_paste_with_byte_limit("中x", 5),
LineEditOutcome::TextChanged
);
assert_eq!(editor.text(), "a中b");
assert_eq!(editor.cursor_byte(), "a中".len());
}
#[test]
fn delete_last_grapheme_ignores_cursor_and_deletes_one_cluster() {
let mut editor = LineEditor::default();
editor.set_text("a👩🏽\u{200d}💻");
let _ = editor.set_cursor_byte(0);
assert_eq!(editor.delete_last_grapheme(), LineEditOutcome::TextChanged);
assert_eq!(editor.text(), "a");
assert_eq!(editor.cursor_byte(), 1);
}
#[test]
fn set_text_sanitizes_and_places_cursor_at_end() {
let mut editor = LineEditor::default();
editor.set_text("one\r\ntwo\nthree\rfour");
assert_eq!(editor.text(), "onetwothreefour");
assert_eq!(editor.cursor_byte(), editor.text().len());
}
#[test]
fn insert_policy_only_gates_insert_commands() {
let mut editor = LineEditor::default();
editor.set_text("ab");
assert_eq!(
editor.handle_key_with_insert_policy(
&key(KeyCode::Char('x'), KeyModifiers::NONE),
|_| false,
),
LineEditOutcome::HandledNoChange
);
assert_eq!(editor.text(), "ab");
assert_eq!(
editor
.handle_key_with_insert_policy(&key(KeyCode::Left, KeyModifiers::NONE), |_| false,),
LineEditOutcome::CursorChanged
);
let mut unrestricted = LineEditor::default();
assert_eq!(
unrestricted.handle_key(&key(KeyCode::Char('\u{202e}'), KeyModifiers::NONE)),
LineEditOutcome::TextChanged
);
assert_eq!(unrestricted.text(), "\u{202e}");
}
#[test]
fn viewport_keeps_graphemes_and_cursor_visible() {
let grapheme = "👩🏽\u{200d}💻";
let mut editor = LineEditor::default();
editor.set_text(format!("a{grapheme}b"));
assert_eq!(
editor.handle_key(&key(KeyCode::Left, KeyModifiers::NONE)),
LineEditOutcome::CursorChanged
);
let viewport = editor.viewport(3);
assert_eq!(
&editor.text()[viewport.visible_byte_range.clone()],
format!("{grapheme}b")
);
assert_eq!(viewport.cursor_display_column, 2);
}
}

View file

@ -2,6 +2,7 @@
pub mod key;
pub mod keyboard_normalizer;
pub(crate) mod line_editor;
#[cfg(target_os = "macos")]
pub mod macos_modifiers;
pub mod mouse;