Publish harness and TUI open-source

initial sync from the monorepo
This commit is contained in:
grokkybara[bot] 2026-07-16 06:46:02 +01:00
commit c68e39f604
2734 changed files with 1437016 additions and 0 deletions

View file

@ -0,0 +1,56 @@
[package]
license = "Apache-2.0"
name = "xai-grok-shared"
edition.workspace = true
[dependencies]
agent-client-protocol = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
dirs = "6"
dunce = { workspace = true }
image = { workspace = true, features = ["png", "jpeg", "gif", "webp"] }
parking_lot = { workspace = true }
prod-mc-cli-chat-proxy-types = { path = "../../../prod/mc/cli-chat-proxy-types" }
regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true, features = ["preserve_order"] }
tempfile = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
urlencoding = { workspace = true }
xai-grok-config-types = { workspace = true }
xai-grok-models = { workspace = true }
xai-grok-tools = { workspace = true }
xai-tty-utils = { workspace = true }
[dev-dependencies]
uuid = { workspace = true, features = ["v4"] }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
# objc2 runtime messaging (links libobjc only, NOT AppKit) for the fast
# NSPasteboard probes and the paste-time native image read in clipboard.rs.
# AppKit itself is dlopen'd lazily at first probe so headless processes never
# load it — preserving the no-AppKit-link rule in clipboard.rs. Version
# matches the workspace lock.
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3"
# arboard for clipboard on non-macOS platforms. On macOS we use pbcopy/pbpaste
# subprocesses to avoid linking AppKit, which causes ~2 GB IOAccelerator GPU
# memory overhead in headless processes (see clipboard.rs).
# `wayland-data-control` makes arboard set the Wayland selection through the
# compositor-side data-control protocol (no surface, no focus) instead of the
# focus-mediated XWayland bridge; no-op off Linux (target-gated inside arboard).
[target.'cfg(not(target_os = "macos"))'.dependencies]
arboard = { workspace = true, features = ["wayland-data-control"] }
# Already in-graph via arboard's wayland-data-control; direct dep for the
# data-control probe in clipboard.rs (`is_primary_selection_supported`).
[target.'cfg(target_os = "linux")'.dependencies]
wl-clipboard-rs = { workspace = true }
[lints]
workspace = true

View file

@ -0,0 +1,37 @@
//! Reproducible harness for benchmarking clipboard attachment reads
//! (the paste hot path).
//!
//! Runs one `get_attachments()` — the same unified probe the pager's paste
//! pipeline executes — and prints the outcome plus wall time. Benchmark the
//! native in-process read against the `osascript` fallback with hyperfine:
//!
//! ```text
//! # put an image on the pasteboard first, e.g.:
//! # osascript -e 'set the clipboard to (read (POSIX file "shot.png") as «class PNGf»)'
//! cargo build --release -p xai-grok-shared --example clipboard_probe
//! hyperfine --warmup 2 \
//! -n native './target/release/examples/clipboard_probe' \
//! -n osascript 'GROK_CLIPBOARD_NO_NATIVE_READ=1 ./target/release/examples/clipboard_probe'
//! ```
//!
//! Exits non-zero when the read errors so hyperfine aborts loudly instead of
//! averaging failures.
fn main() -> anyhow::Result<()> {
let started = std::time::Instant::now();
let attachments = xai_grok_shared::clipboard::get_attachments()?;
let elapsed_ms = started.elapsed().as_secs_f64() * 1e3;
let image = attachments
.image
.map(|img| format!("{} ({} bytes)", img.mime_type, img.data.len()));
let file_urls = attachments
.file_urls
.map(|urls| format!("{} path(s)", urls.lines().count()));
println!(
"{elapsed_ms:.1} ms image={} file_urls={}",
image.as_deref().unwrap_or("none"),
file_urls.as_deref().unwrap_or("none"),
);
Ok(())
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
//! Shared utilities used by both `xai-grok-shell` and its downstream clients
//! (e.g. `xai-grok-pager-render`). This crate sits upstream of `xai-grok-shell`
//! so it must never depend on it.
pub mod clipboard;
pub mod placeholder_images;
pub mod session;
pub mod stderr;
pub mod ui_config;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
use agent_client_protocol as acp;
/// Session identity: `id` + `cwd`.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Info {
pub id: acp::SessionId,
pub cwd: String,
}

View file

@ -0,0 +1,13 @@
use std::path::PathBuf;
pub mod info;
pub use info::Info;
// Re-export shared feedback wire types used by downstream crates
// (e.g. xai-grok-pager-render).
pub use prod_mc_cli_chat_proxy_types::feedback_types::FeedbackTerminalInfo;
pub fn session_dir(info: &Info) -> PathBuf {
xai_grok_tools::util::grok_home::sessions_cwd_dir(&info.cwd).join(info.id.to_string())
}

View file

@ -0,0 +1,50 @@
//! Serialized access to the TUI's stderr writer.
use parking_lot::{Mutex, MutexGuard};
use std::sync::OnceLock;
static STDERR_OUTPUT_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn stderr_output_lock() -> &'static Mutex<()> {
STDERR_OUTPUT_LOCK.get_or_init(|| Mutex::new(()))
}
pub fn stderr_lock() -> MutexGuard<'static, ()> {
stderr_output_lock().lock()
}
/// Execute `f` with exclusive access to the TUI's stderr writer.
///
/// When [`xai_tty_utils::redirect_native_stderr`] has been called, this
/// writes to the dup'd fd that points at the real terminal (bypassing the
/// `/dev/null` redirect on fd 2). Otherwise falls back to normal stderr.
pub fn with_locked_stderr<T>(f: impl FnOnce(&mut std::fs::File) -> T) -> T {
let _guard = stderr_lock();
let mut file = xai_tty_utils::dup_tui_stderr().unwrap_or_else(|_| {
// Fallback: try_clone stderr to get an independently-owned
// File. This path is hit if redirect_native_stderr was never
// called or fd dup fails.
let stderr = std::io::stderr();
let stderr_file: std::fs::File;
#[cfg(unix)]
{
use std::os::unix::io::{AsRawFd, FromRawFd};
// SAFETY: stderr fd (2) is valid; from_raw_fd takes ownership
// of the dup'd copy, not the original.
let fd = unsafe { libc::dup(stderr.as_raw_fd()) };
stderr_file = unsafe { std::fs::File::from_raw_fd(fd) };
}
#[cfg(not(unix))]
{
use std::os::windows::io::{AsRawHandle, FromRawHandle};
// SAFETY: stderr handle is valid; DuplicateHandle gives us
// an independent copy.
let handle = stderr.as_raw_handle();
let temp = unsafe { std::fs::File::from_raw_handle(handle) };
stderr_file = temp.try_clone().expect("dup stderr fallback");
std::mem::forget(temp);
}
stderr_file
});
f(&mut file)
}

View file

@ -0,0 +1,369 @@
use serde::{Deserialize, Serialize};
use xai_grok_config_types::DisplayRefreshSettings;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UiConfig {
pub max_thoughts_width: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
/// Model ID to use for the secondary agent when forking.
/// Defaults to the main default model (from default_models.json).
pub fork_secondary_model: String,
/// YOLO mode. Read by `util::config`, declared here for `serde_ignored`.
#[serde(default)]
pub yolo: bool,
/// UI theme alias. Read by `util::config`, declared here for `serde_ignored`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ui_theme: Option<String>,
/// Compact mode. Read by pager, declared here for `serde_ignored`.
#[serde(default)]
pub compact_mode: bool,
/// Simple mode. Read by pager, declared here for `serde_ignored`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub simple_mode: Option<bool>,
/// Read by `load_permission_mode()`. Declared for `serde_ignored`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
/// Legacy name for `permission_mode`. Declared for `serde_ignored`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval_mode: Option<String>,
/// Which permission option the cursor preselects on the **first**
/// permission prompt of a session. One of `allow_once`, `allow_always`,
/// or `reject`. After the first prompt, the cursor sticks to the user's
/// last-used option kind. When unset, the first prompt preselects the
/// "Always allow on all sessions" (enable-always-approve) row. Read by
/// the pager's permission view.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_selected_permission: Option<String>,
/// Written by the pager's appearance persist module.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_timestamps: Option<bool>,
/// Timeline sidebar (per-turn tick rail in place of the scrollbar).
/// `None` = off (client default; opt-in). Written by the pager's settings modal.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_timeline: Option<bool>,
/// Theme to use when the OS is in dark mode. Written by the pager's theme persist module.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_dark_theme: Option<String>,
/// Theme to use when the OS is in light mode. Written by the pager's theme persist module.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_light_theme: Option<String>,
/// Mouse-wheel and trackpad scroll speed multiplier (1100).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scroll_speed: Option<u8>,
/// Force scroll input classification (`auto` | `wheel` | `trackpad`).
/// Written by the pager's settings modal; unset defaults to `auto`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scroll_mode: Option<String>,
/// Invert vertical scroll direction ("natural" scrolling).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invert_scroll: Option<bool>,
/// Lines per scroll tick, applied to BOTH wheel and trackpad pricing
/// (110). Unset keeps the per-terminal scroll profile's values.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scroll_lines: Option<u8>,
/// Vim-style scrollback navigation (hjkl, gg/G, /).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vim_mode: Option<bool>,
/// How ` ```mermaid ` code blocks are rendered (`auto` | `on` | `off`).
/// Written by the pager's settings modal.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub render_mermaid: Option<String>,
/// Hunk-tracker mode the pager advertises to the agent (`agent_only` |
/// `all_dirty` | `off`). Written by the pager's settings modal; read at
/// connect time (CLI `--hunk-tracker-mode` / `GROK_HUNK_TRACKER` override
/// it). `off` disables hunk tracking entirely.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hunk_tracker_mode: Option<String>,
/// Voice capture chord behavior: `toggle` or `hold` (hold-to-talk; needs a
/// Kitty-protocol terminal, else falls back to toggle). Written by the
/// settings modal; unset defaults to `hold`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub voice_capture_mode: Option<String>,
/// Speech-to-text language preference for voice dictation. A Grok STT
/// catalog code (`en`, `es`, `ja`, … — see xAI STT supported languages) or
/// `auto` (system locale, resolved at connect). Written by the settings
/// modal; unset leaves `[voice].language` / default `en`. When set, overrides
/// `[voice].language` for the session.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub voice_stt_language: Option<String>,
/// When `true`, registers `Ctrl+R` (while scrollback is focused) to toggle
/// terminal mouse reporting (mouse capture) so users can hand selection back
/// to the terminal for native click-drag copy/paste. Opt-in only; unset/false
/// leaves mouse reporting always on with no toggle shortcut. The prompt keeps
/// `Ctrl+R` for history search — focus scrollback (Esc/Tab) first.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mouse_reporting_toggle: Option<bool>,
/// When cancelling a parent turn with running subagents: `always_stop` stops
/// them without prompting, `always_continue` leaves them running without
/// prompting. Unset/`ask` shows the cancel-turn picker. Written by the pager
/// when the user picks "Always stop" / "Always continue".
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancel_subagents_on_turn_cancel: Option<String>,
/// User knob for the `remember_tool_approvals` gate: when `true`, permission
/// prompts show the granular per-tool "Always allow …" options. Written by
/// the settings modal; requirements/env/managed/remote settings also feed the
/// effective gate.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remember_tool_approvals: Option<bool>,
/// In-app drag selection highlight: `flash` | `hold` (legacy bool accepted).
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_keep_text_selection"
)]
pub keep_text_selection: Option<String>,
/// Legacy TTL ms; only `Some(0)` counts when `keep_text_selection` is unset.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selection_highlight_duration_ms: Option<u64>,
/// Show agent thinking/reasoning blocks in the TUI scrollback.
/// `None` = on (client default). Written by the pager's settings modal.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_thinking_blocks: Option<bool>,
/// Fold runs of consecutive non-destructive tool calls (reads, searches,
/// lists) into one transcript row. `None` = on (client default). Written
/// by the pager's settings modal.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group_tool_verbs: Option<bool>,
/// Show Edit tool calls as a collapsed one-line `+N/-M` diffstat summary
/// by default (expand for the diff). `None` = off (client default).
/// Written by the pager's settings modal.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collapsed_edit_blocks: Option<bool>,
/// Next-prompt suggestions (tab autocomplete ghost text) after each turn.
/// `None` = on (client default). Written by the pager's settings modal;
/// the `GROK_PROMPT_SUGGESTIONS` env var overrides at runtime.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_suggestions: Option<bool>,
/// Startup cursor style: `None` (default) inherits the terminal's own
/// style; `Some(true)` forces the legacy blinking block, `Some(false)` a
/// steady block. Config-file-only knob (no /settings row).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor_blink: Option<bool>,
/// `"fullscreen"` | `"minimal"`; unset → product default fullscreen.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub screen_mode: Option<String>,
/// Retired hidden opt-in for terminal-like double/triple-click word/line
/// selection. Superseded by `keep_text_selection = "word_select"`. Still
/// read only when `keep_text_selection` is unset; Settings clears this on
/// write. `"word_select"` | unset.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub double_click_action: Option<String>,
/// Per-tip contextual-hint opt-outs (`[ui.contextual_hints]`). Each `None`
/// inherits the remote/default (on); `Some` is a user-explicit choice that
/// beats the remote tier. Skipped on the wire when untouched so the section
/// only appears once a user toggles a tip.
#[serde(default, skip_serializing_if = "ContextualHints::is_default")]
pub contextual_hints: ContextualHints,
/// Display-refresh probe + auto-cadence (`[ui.display_refresh]`). Per-field
/// `None` inherits remote/default; skipped when untouched.
#[serde(default, skip_serializing_if = "DisplayRefreshSettings::is_default")]
pub display_refresh: DisplayRefreshSettings,
}
/// User-config opt-outs for the per-tip contextual hints, serialized as
/// `[ui.contextual_hints]`. Per-field `None` means "inherit remote/default";
/// `Some(bool)` is a user-explicit choice (needed so the resolver can let it
/// beat the remote tier).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextualHints {
/// Undo tip (Ctrl+Z after a substantial draft wipe).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub undo: Option<bool>,
/// Plan-mode nudge (typing a planning keyword).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plan_mode: Option<bool>,
/// Clipboard-image input tip.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_input: Option<bool>,
/// Send-now tip after queuing a mid-turn follow-up (InterjectPrompt chord).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub send_now: Option<bool>,
/// Small-screen tip (`/compact-mode` hint on smallish terminals).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub small_screen: Option<bool>,
/// Word-select tip after double-clicking scrollback while Text selection
/// is still fold/nav (`flash` / `hold`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub word_select: Option<bool>,
}
impl ContextualHints {
/// True when no tip has a user-explicit value (all inherit). Lets the
/// section stay absent from `config.toml` until the user toggles a tip.
pub fn is_default(&self) -> bool {
self.undo.is_none()
&& self.plan_mode.is_none()
&& self.image_input.is_none()
&& self.send_now.is_none()
&& self.small_screen.is_none()
&& self.word_select.is_none()
}
}
const DEFAULT_MAX_THOUGHTS_WIDTH: u16 = 120;
fn deserialize_keep_text_selection<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Bool(bool),
Str(String),
}
Ok(
Option::<Raw>::deserialize(deserializer)?.map(|raw| match raw {
Raw::Bool(true) => "hold".to_string(),
Raw::Bool(false) => "flash".to_string(),
Raw::Str(s) => s,
}),
)
}
impl Default for UiConfig {
fn default() -> Self {
Self {
max_thoughts_width: DEFAULT_MAX_THOUGHTS_WIDTH,
theme: None,
fork_secondary_model: xai_grok_models::default_model().to_string(),
yolo: false,
ui_theme: None,
compact_mode: false,
simple_mode: None,
permission_mode: None,
approval_mode: None,
default_selected_permission: None,
show_timestamps: None,
show_timeline: None,
auto_dark_theme: None,
auto_light_theme: None,
scroll_speed: None,
scroll_mode: None,
invert_scroll: None,
scroll_lines: None,
vim_mode: None,
render_mermaid: None,
hunk_tracker_mode: None,
voice_capture_mode: None,
voice_stt_language: None,
mouse_reporting_toggle: None,
remember_tool_approvals: None,
cancel_subagents_on_turn_cancel: None,
keep_text_selection: None,
selection_highlight_duration_ms: None,
show_thinking_blocks: None,
group_tool_verbs: None,
collapsed_edit_blocks: None,
prompt_suggestions: None,
cursor_blink: None,
screen_mode: None,
double_click_action: None,
contextual_hints: ContextualHints::default(),
display_refresh: DisplayRefreshSettings::default(),
}
}
}
impl UiConfig {
/// The single source of truth for the timeline-sidebar default (opt-in).
/// Flip this one line to change the default everywhere.
///
// TODO: migrate the other boolean UI settings (show_timestamps,
// simple_mode, show_thinking_blocks, …) to the same const + resolver
// pattern. They currently duplicate their default literal across
// cache.rs / config.rs / defs.rs / setters.rs / registry.rs and rely on
// the registry drift-guard test to catch mismatches.
pub const SHOW_TIMELINE_DEFAULT: bool = false;
/// Resolved timeline-sidebar setting: the configured value, or
/// [`Self::SHOW_TIMELINE_DEFAULT`] when unset. The one place the default
/// is applied — every layer (cache, appearance config, settings modal)
/// reads through here so they cannot drift.
pub fn show_timeline_enabled(&self) -> bool {
self.show_timeline.unwrap_or(Self::SHOW_TIMELINE_DEFAULT)
}
/// True when the highlight should not timer-dismiss (`hold` / `word_select`,
/// or legacy duration 0).
pub fn keep_text_selection_enabled(&self) -> bool {
if let Some(ref s) = self.keep_text_selection {
return s == "hold" || s == "word_select";
}
matches!(self.selection_highlight_duration_ms, Some(0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keep_text_selection_enabled_precedence() {
let mut ui = UiConfig::default();
assert!(!ui.keep_text_selection_enabled());
ui.selection_highlight_duration_ms = Some(0);
assert!(ui.keep_text_selection_enabled());
ui.selection_highlight_duration_ms = Some(150);
assert!(!ui.keep_text_selection_enabled());
ui.selection_highlight_duration_ms = Some(0);
ui.keep_text_selection = Some("flash".into());
assert!(!ui.keep_text_selection_enabled());
ui.keep_text_selection = Some("hold".into());
ui.selection_highlight_duration_ms = Some(999);
assert!(ui.keep_text_selection_enabled());
ui.keep_text_selection = Some("hold".into());
ui.selection_highlight_duration_ms = None;
assert!(ui.keep_text_selection_enabled());
// `word_select` implies hold (persistent highlight).
ui.keep_text_selection = Some("word_select".into());
ui.selection_highlight_duration_ms = None;
assert!(ui.keep_text_selection_enabled());
}
#[test]
fn keep_text_selection_deserializes_legacy_bool_and_string() {
let from_true: UiConfig = serde_json::from_str(r#"{"keep_text_selection": true}"#).unwrap();
assert_eq!(from_true.keep_text_selection.as_deref(), Some("hold"));
let from_false: UiConfig =
serde_json::from_str(r#"{"keep_text_selection": false}"#).unwrap();
assert_eq!(from_false.keep_text_selection.as_deref(), Some("flash"));
let from_hold: UiConfig =
serde_json::from_str(r#"{"keep_text_selection": "hold"}"#).unwrap();
assert_eq!(from_hold.keep_text_selection.as_deref(), Some("hold"));
let from_flash: UiConfig =
serde_json::from_str(r#"{"keep_text_selection": "flash"}"#).unwrap();
assert_eq!(from_flash.keep_text_selection.as_deref(), Some("flash"));
}
#[test]
fn display_refresh_nested_deserialize() {
let ui: UiConfig = serde_json::from_str(
r#"{"display_refresh": {"auto_cadence_enabled": true, "floor_ms": 7, "probe_enabled": false}}"#,
)
.unwrap();
assert_eq!(ui.display_refresh.auto_cadence_enabled, Some(true));
assert_eq!(ui.display_refresh.floor_ms, Some(7));
assert_eq!(ui.display_refresh.probe_enabled, Some(false));
assert!(!ui.display_refresh.is_default());
}
#[test]
fn display_refresh_default_is_skipped_shape() {
assert!(DisplayRefreshSettings::default().is_default());
let ui = UiConfig::default();
assert!(ui.display_refresh.is_default());
}
}