Grok Build▹
Deep Dive · Grok Build

运行时与 TUI 入口

从启动器、重连计划、TTY 交接看 Grok Build 怎样把终端体验做成可恢复运行时。

main.rs ├─ renderer/hooks ├─ parse args └─ dispatch
§ 00 · DETAIL

启动器

入口先准备渲染、内存钩子、崩溃处理和 telemetry,再进入参数解析。关键线索在 sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1456、sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1489、sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1530、sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1597。这说明它把运行环境准备和命令分派分层,而不是直接启动一个交互循环。

▸启动准备

sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1456-1528Rust
1456fn main() {
1457    xai_grok_pager_minimal::install();
1458    #[cfg(all(feature = "jemalloc", unix))]
1459    xai_grok_pager::memory_release::install_release_hook(purge_jemalloc_retained_pages);
1460    #[cfg(all(feature = "jemalloc", unix))]
1461    {
1462        xai_grok_pager::memory_trace::install_allocator_stats_provider(jemalloc_allocator_stats);
1463        xai_grok_pager::memory_trace::install_allocator_dump_provider(jemalloc_stats_dump);
1464    }
1465    #[cfg(all(feature = "jemalloc", unix))]
1466    install_heap_profile_hooks();
1467    if let Some(code) = xai_grok_pager::app::mermaid_worker::maybe_run_render_subprocess() {
1468        std::process::exit(code);
1469    }
1470    xai_grok_pager::memory_trace::start(
1471        xai_grok_shell::util::grok_home::grok_home().join("memtrace"),
1472    );
1473    raise_fd_limit();
1474    if let Err(e) = xai_grok_config::validate_requirements() {
1475        eprintln!("Couldn't start Grok: {e}");
1476        eprintln!();
1477        eprintln!(
1478            "Update Grok to a version the policy allows, or ask your administrator \
1479             to fix the managed requirements."
1480        );
1481        std::process::exit(2);
1482    }
1483    let _sentry_guard = xai_grok_telemetry::sentry::init(xai_grok_telemetry::sentry::Config {
1484        client: "grok-pager",
1485        client_version: PAGER_CLIENT_VERSION,
1486        release: env!("VERSION_WITH_COMMIT"),
1487        disabled: xai_grok_shell::agent::config::is_error_reporting_disabled_sync(),
1488    });
1489    xai_grok_pager::docs::extract_user_guide_docs(&xai_grok_shell::util::grok_home::grok_home());
1490    xai_crash_handler::install_terminal_restore_only();
1491    if xai_grok_shell::util::config::load_crash_handler_enabled_sync() {
1492        let crash_dir = xai_grok_shell::util::grok_home::grok_home().join("crash");
1493        if let Some(report) = xai_crash_handler::check_previous_crash(&crash_dir) {
1494            eprintln!("Grok crashed during your last session.");
1495            eprintln!("  Signal:  {}", report.signal_name);
1496            eprintln!("  Version: {}", report.app_version);
1497            eprintln!("  Report:  {}", report.report_path.display());
1498            eprintln!();
1499        }
1500        if !xai_crash_handler::install(xai_crash_handler::CrashHandlerConfig {
1501            app_version: env!("VERSION_WITH_COMMIT").to_string(),
1502            crash_dir: crash_dir.clone(),
1503        }) {
1504            eprintln!(
1505                "warning: crash handler enabled but failed to install (check permissions on {})",
1506                crash_dir.display()
1507            );
1508        }
1509    }
1510    let crashed = xai_grok_shell::active_sessions::collect_crashed().unwrap_or_default();
1511    if !crashed.is_empty() {
1512        tracing::info!(
1513            count = crashed.len(),
1514            "Found crashed sessions from a previous run"
1515        );
1516    }
1517    let runtime = tokio::runtime::Builder::new_multi_thread()
1518        .enable_all()
1519        .build()
1520        .unwrap_or_else(|e| panic!("failed to start tokio runtime: {e}"));
1521    let result = run_and_shutdown(runtime, async_main(), RUNTIME_SHUTDOWN_GRACE);
1522    xai_grok_telemetry::debug_log::flush();
1523    if let Err(e) = result {
1524        xai_tty_utils::restore_native_stderr();
1525        eprintln!("Error: {e:#}");
1526        drop(_sentry_guard);
1527        std::process::exit(1);
1528    }

▸参数解析

sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1530-1598Rust
1530async fn async_main() -> Result<()> {
1531    let _ = rustls::crypto::ring::default_provider().install_default();
1532    let mut args = PagerArgs::parse_and_apply_cwd()?;
1533    if let Some(ref mode) = args.compaction_mode {
1534        unsafe { std::env::set_var("GROK_COMPACTION_MODE", mode) };
1535    }
1536    if let Some(ref detail) = args.compaction_detail {
1537        unsafe { std::env::set_var("GROK_COMPACTION_DETAIL", detail) };
1538    }
1539    if args.chat() {
1540        unsafe {
1541            std::env::set_var(xai_grok_shell::agent::chat_modes::GROK_CHAT_MODE_ENV, "1");
1542        }
1543    }
1544    if let Some(ref socket) = args.leader_socket {
1545        unsafe { std::env::set_var(xai_grok_shell::leader::LEADER_SOCKET_ENV, socket) };
1546    }
1547    if let Some(ref path) = args.debug_file {
1548        unsafe {
1549            std::env::set_var("GROK_DEBUG_LOG", path);
1550            std::env::remove_var("GROK_LOG_FILE");
1551        }
1552    }
1553    if args.debug || args.debug_file.is_some() {
1554        let set_if_unset = |k: &str, v: &str| {
1555            if std::env::var_os(k).is_none() {
1556                unsafe { std::env::set_var(k, v) };
1557            }
1558        };
1559        set_if_unset("GROK_DEBUG_LOG", "1");
1560        set_if_unset("GROK_HOOKS_LOG", "1");
1561    }
1562    if let Some(Command::Completions { shell }) = &args.command {
1563        xai_grok_pager::completions_cmd::run(*shell);
1564        return Ok(());
1565    }
1566    if let Some(Command::Wrap(ref wrap_args)) = args.command {
1567        return xai_grok_pager::wrap_cmd::run(wrap_args);
1568    }
1569    let saved_profile = args.saved_resume_profile();
1570    let sandbox_profile_arg = match args.startup_sandbox_profile(saved_profile.as_deref()) {
1571        xai_grok_pager::app::cli::SandboxStartup::Apply(profile) => profile,
1572        xai_grok_pager::app::cli::SandboxStartup::Conflict { requested, saved } => {
1573            eprintln!(
1574                "error: cannot resume this session under sandbox profile '{requested}' — \
1575                 it was created with '{saved}'. Omit --sandbox to resume with '{saved}', \
1576                 or start a new session to use '{requested}'."
1577            );
1578            std::process::exit(1);
1579        }
1580    };
1581    xai_grok_shell::config::apply_sandbox(
1582        None,
1583        sandbox_profile_arg.as_deref(),
1584        args.cwd.as_deref(),
1585    );
1586    flag_dashboard_at_startup_if_requested(&mut args)?;
1587    let is_interactive = args.command.is_none()
1588        && args.single.is_none()
1589        && args.prompt_json.is_none()
1590        && args.prompt_file.is_none();
1591    xai_grok_shell::http::set_client_name(if is_interactive {
1592        xai_grok_workspace::permission::ClientType::GrokPager
1593    } else {
1594        xai_grok_workspace::permission::ClientType::Generic
1595    });
1596    let update_config = build_update_config();
1597    if let Some(command) = args.command.take() {
1598        match command {

继续向下看,sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1598 是顶层命令分派起点,sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1700 仍在同一 command surface 内处理子命令;TUI 侧 sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:320 和 sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:411 则继续证明事件循环负责的是长期会话,而不是一次性命令。

§ 01 · DETAIL

重连恢复

TUI 的关键是恢复已有 agent 状态。sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:97 构造 reconnect plan,sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:115 保存 cwd/yolo/auto/cursor,sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:126 汇总恢复结果,sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:153 继续处理 folder trust。读这几段可以看到产品把“恢复”当成一等流程。

▸恢复计划

sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:97-150Rust
97fn plan_reconnect_load(
98    agent: &super::agent_view::AgentView,
99    fallback_cwd: &std::path::Path,
100) -> Option<ReconnectLoadPlan> {
101    let session_id = agent.session.session_id.clone()?;
102    let cwd = if agent.session.cwd.as_os_str().is_empty() {
103        fallback_cwd.to_path_buf()
104    } else {
105        agent.session.cwd.clone()
106    };
107    let yolo = agent.session.is_yolo();
108    // Set BOTH yoloMode and autoMode explicitly. The leader's capability injection
109    // only fills ABSENT keys, so omitting autoMode here lets a stale launch-time
110    // `ClientCapabilities.auto_mode` re-enable Auto after the user left it (e.g.
111    // Shift+Tab to Ask). Auto is per-agent (symmetric with yolo) — derive it from
112    // this agent's own `auto_mode` so a background tab reconnects with ITS mode,
113    // not the active tab's global `current_ui` mirror.
114    let auto = super::dispatch::effective_auto(yolo, agent.session.is_auto());
115    let mut meta = serde_json::json!({ "yoloMode": yolo, "autoMode": auto });
116    if let Some(ref cursor) = agent.last_seen_event_id {
117        meta["cursor"] = serde_json::Value::String(cursor.clone());
118    }
119    Some(ReconnectLoadPlan {
120        session_id,
121        cwd,
122        meta,
123    })
124}
125
126/// Resolve the two post-reconnect restore outcomes from the per-agent
127/// `session/load` results.
128///
129/// - `all_restored` (AND across every reloaded tab, plus `init_ok`) drives the
130///   user-facing toast: it reports whether the WHOLE reconnect came back.
131/// - `active_restored` is per-agent: the ACTIVE tab's OWN reload succeeded. It
132///   gates that tab's post-reconnect queue drain. Gating the drain on
133///   `all_restored` would let one failed background tab strand prompts queued
134///   on a healthy active tab — the drain (`dispatch_drain_queue`) only ever
135///   touches the active agent, so a background failure has no bearing on it.
136///
137/// `loads` maps each reloaded agent to `(success, running_prompt_id)`; an agent
138/// in `pending_agent_ids` but absent from `loads` is treated as failed
139/// (mirrors the `unwrap_or((false, _))` at the finalize site).
140fn reconnect_restore_outcome(
141    init_ok: bool,
142    pending_agent_ids: &[super::agent::AgentId],
143    loads: &std::collections::HashMap<super::agent::AgentId, (bool, Option<String>)>,
144    active_agent_id: Option<super::agent::AgentId>,
145) -> (bool, bool) {
146    let load_ok = |id: &super::agent::AgentId| -> bool { loads.get(id).is_some_and(|(ok, _)| *ok) };
147    let all_restored = init_ok && pending_agent_ids.iter().all(load_ok);
148    let active_restored = init_ok
149        && active_agent_id.is_some_and(|aid| pending_agent_ids.contains(&aid) && load_ok(&aid));
150    (all_restored, active_restored)

▸信任状态

sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:153-193Rust
153/// Compute the folder-trust verdict for the session cwd and seed
154/// [`AppView::trust_state`]. Pager-side mirror of the agent's resolve: read the
155/// local store, scan for repo-local code-exec config, and run the pure
156/// [`decide`](xai_grok_workspace::folder_trust::decide) precedence.
157///
158/// `TrustOutcome::Prompt` (interactive + untrusted + repo configs present)
159/// becomes `TrustState::Pending` (show the question); everything else becomes
160/// `TrustState::Done`. The feature-off fast path (kill-switch / opt-out /
161/// local build) short-circuits before any I/O.
162fn seed_trust_state(
163    app: &mut AppView,
164    remote: Option<&xai_grok_shell::util::config::RemoteSettings>,
165) {
166    use std::io::IsTerminal;
167    use xai_grok_workspace::folder_trust::{
168        TrustOutcome, decide, decide_inputs_with_interactive, feature_enabled,
169    };
170    use xai_grok_workspace::trust::workspace_key;
171
172    let feature = feature_enabled(remote);
173    if !feature {
174        app.trust_state = TrustState::Done;
175        return;
176    }
177
178    // The cwd the user launched in == the process cwd == `app.cwd` (set at
179    // construction), matching the `--trust` grant's `std::env::current_dir()`.
180    let cwd = app.cwd.clone();
181    let key = workspace_key(&cwd);
182    // Reuse the canonical gather (store trust + repo-config scan) but pass the
183    // pager's stdin-only interactivity: the TUI prompts via the rendered
184    // question + crossterm keyboard, NOT stderr (the pager redirects native
185    // stderr at startup, so the engine's `stdin && stderr` would be false here
186    // and the question would never show). TTY stdin => user can answer;
187    // otherwise fail closed (no prompt).
188    let inputs = decide_inputs_with_interactive(&cwd, &key, std::io::stdin().is_terminal());
189    app.trust_state = match decide(feature, &inputs) {
190        TrustOutcome::Prompt => TrustState::Pending { workspace: key },
191        TrustOutcome::Trusted | TrustOutcome::Untrusted => TrustState::Done,
192    };
193}
§ 02 · DETAIL

终端交接

当 agent 需要让子进程接管终端时,事件循环显式暂停渲染并恢复 raw mode/alt screen。sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:195 到 sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:260 是避免 TUI 与 shell 输出互相污染的核心,sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1777 又显示 headless 单轮走的是同一启动器之外的独立分支。

▸子进程暂停

sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:195-260Rust
195/// Suspend the inline TUI, run a blocking child that takes over the tty
196/// (`$EDITOR`, `$PAGER`, …), then restore. Shared by the editor and transcript
197/// suspend paths so the subtle reader-park + raw-mode + alt-screen handoff lives
198/// in one place.
199///
200/// The reader thread is parked first so the child (which inherits this tty)
201/// keeps every keystroke instead of racing the reader; on return, buffered
202/// terminal query replies and any pre-park keystroke are drained before the
203/// reader resumes.
204///
205/// The frame **writer** thread is then drained (bounded) before the child
206/// starts: frames are written to the tty asynchronously, and the frame that
207/// armed this suspend (e.g. minimal's final `/transcript` pump slice) is
208/// typically queued microseconds before we get here. Un-drained, those bytes
209/// race the child's own output — they can land on the child's alternate
210/// screen (so the main screen never receives them; on an inline viewport a
211/// commit's scroll then leaves every following row off by its height) or tear
212/// around the alt-screen switch, printing escape fragments (`[`…) that the
213/// renderer's diff can't see and thus never repairs.
214///
215/// In minimal mode the physical cursor is probed (`ESC[6n`) right before the
216/// child runs and right after it exits — while the reader thread is still
217/// parked, so the replies can't be stolen. The returned "cursor after the
218/// child, iff it moved" tells the caller whether the child *printed to the
219/// main screen* (cat-style pager: cursor left below its output) or *restored
220/// it* (less-style alt-screen pager: `rmcup` puts the cursor back exactly
221/// where it was): the caller re-anchors the live region below the new output
222/// in the first case and repaints in place in the second.
223fn suspend_for_child(
224    screen_mode: crate::app::ScreenMode,
225    writer_sync: &crate::render::draw::WriterSync,
226    input_paused: &std::sync::atomic::AtomicBool,
227    reader_parked: &std::sync::atomic::AtomicBool,
228    input_rx: &mut tokio::sync::mpsc::UnboundedReceiver<crossterm::event::Event>,
229    run_child: impl FnOnce(),
230) -> Option<(u16, u16)> {
231    use std::sync::atomic::Ordering;
232    // Pause the reader thread, then wait for a FRESH park (reader provably out of
233    // crossterm) so the main thread is the sole poll/read caller; bounded so a
234    // dead reader can't hang us.
235    input_paused.store(true, Ordering::Release);
236    reader_parked.store(false, Ordering::Release);
237    let park_deadline = std::time::Instant::now() + Duration::from_millis(500);
238    while !reader_parked.load(Ordering::Acquire) && std::time::Instant::now() < park_deadline {
239        std::thread::sleep(Duration::from_millis(5));
240    }
241    // Every queued frame must be ON the tty before the child takes it (and, in
242    // fullscreen, before LeaveAlternateScreen below — mirroring teardown's
243    // "no late frame after LeaveAlternateScreen" drain). Bounded like the
244    // reader park so a wedged pty can't hang the suspend.
245    if !writer_sync.wait_drained(Duration::from_millis(750)) {
246        tracing::warn!("suspend: frame writer not drained within 750ms; proceeding");
247    }
248    // Pre-child cursor probe (minimal only — minimal's startup already proved
249    // this terminal answers CPR). Reader is parked, so the reply is ours.
250    let pre_cursor = screen_mode
251        .is_minimal()
252        .then(|| crossterm::cursor::position().ok())
253        .flatten();
254    if screen_mode.is_fullscreen() {
255        xai_grok_shell::util::with_locked_stderr(|stderr| {
256            let _ = crossterm::execute!(stderr, crossterm::terminal::LeaveAlternateScreen);
257        });
258    }
259    let _ = crossterm::terminal::disable_raw_mode();
260    run_child();

▸Headless 单轮

sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1777-1825Rust
1777    let headless_prompt = xai_grok_pager::headless::HeadlessPrompt::from_args(
1778        args.single.as_deref(),
1779        args.prompt_json.as_deref(),
1780        args.prompt_file.as_deref(),
1781    )?;
1782    if let Some(prompt) = headless_prompt {
1783        init_tracing_simple(HEADLESS_ENTRYPOINT);
1784        let _otel_guard = xai_grok_telemetry::otel_layer::otel_guard();
1785        enforce_minimum_version_or_exit(&update_config).await;
1786        let launch_yolo = xai_grok_shell::util::config::effective_yolo_for_launch(
1787            args.yolo,
1788            args.permission_mode_flag.as_deref(),
1789            None,
1790        );
1791        if let Some(warning) = launch_yolo.blocked_warning {
1792            eprintln!("grok: {warning}");
1793        }
1794        let json_schema = args
1795            .json_schema
1796            .as_deref()
1797            .map(xai_grok_pager::headless::parse_json_schema)
1798            .transpose()?;
1799        if json_schema.is_some() {
1800            if args.output_format == xai_grok_pager::headless::OutputFormat::Plain {
1801                args.output_format = xai_grok_pager::headless::OutputFormat::Json;
1802            }
1803            if args.self_verify {
1804                anyhow::bail!(
1805                    "--json-schema and --self-verify cannot be used together: \
1806                     verification output would corrupt the structured response"
1807                );
1808            }
1809        }
1810        return xai_grok_pager::headless::run_single_turn(
1811            prompt,
1812            args.verbatim,
1813            xai_grok_pager::headless::HeadlessOptions {
1814                session_id: args.session_id.clone(),
1815                resume: args.resume_session.or(args.load_session),
1816                cwd: args.cwd,
1817                yolo: launch_yolo.yolo,
1818                trust: args.trust,
1819                output_format: args.output_format,
1820                json_schema,
1821                model: args.model,
1822                rules: args.rules,
1823                system_prompt_override: args.system_prompt_override.clone(),
1824                continue_last_session: args.continue_last_session,
1825                fork_session: args.fork_session,
Generated · 2026-07-16xai-org/grok-build · source-level teardown