Synced from monorepo
Synced from monorepo Changes: - Refresh tool search when the managed MCP catalog is re-fetched - Prevent duplicate leader process spawn and startup hang from stale leaders - Document marketplaces, plugins, and organization controls - Stamp session ID on image generation direct-to-API requests - Fix auto mode blocked documentation - Auto mode considers recent user intent - Expose deploy archive, taken-down, limit, and in-progress reasons on the chat API - Fail-closed auth refresh contract for shell clients - Emit a chat-supplied per-session turn index in turn hooks - Show bash mode chrome in minimal mode - Add metrics for true-noop and stationarity stops - Include voice interim text on prompt submit - Silently end turn on true-noop thrash - Quiet copy toast when clipboard delivery is confirmed - Fix session fork truncating at the wrong prompt in rewound sessions - Make the idle "still running" watcher cue clickable to open the tasks pane - Default web search model to grok-4.5 - Let plugin subagents inherit parent MCP servers - Gate no-op end-turn reminder on system reminders - Add gateway bridge lifecycle telemetry - Allow editing finalized text while voice is open - Relocate token carrier to turn-commit events and plumb per-turn origin context - Raise workflow scratch quotas and make failed runs resumable - Workflows overlay: auto-progress phases, live agent status, and drop budget meter Source-Revision: 9b8d35b46d959c042ea9aa31cbbebbd1f0c5c527
This commit is contained in:
parent
69f0ba880a
commit
6e38642082
103 changed files with 4964 additions and 1261 deletions
|
|
@ -63,8 +63,15 @@ fn paintable_btw_area(frame_area: Rect, area: Rect) -> Option<Rect> {
|
|||
///
|
||||
/// Shared with [`super::overlay::sync_viewport`] so viewport sizing measures the
|
||||
/// prompt's height exactly as the live region will draw it.
|
||||
///
|
||||
/// `input_mode` wires special composer modes (bash `! `, feedback `~ `,
|
||||
/// remember `# `) the same way the full TUI does — without this, `!` on an
|
||||
/// empty prompt would flip mode invisibly (key consumed, default `❯` remains).
|
||||
pub(super) fn prompt_style(
|
||||
appearance: &xai_grok_pager::appearance::AppearanceConfig,
|
||||
input_mode: xai_grok_pager::app::agent_view::PromptInputMode,
|
||||
theme: &Theme,
|
||||
multiline: bool,
|
||||
) -> PromptStyle {
|
||||
PromptStyle {
|
||||
focused: true,
|
||||
|
|
@ -75,10 +82,10 @@ pub(super) fn prompt_style(
|
|||
chrome_pad_left: live_left_inset(appearance),
|
||||
chrome_pad_right: 0,
|
||||
bg_override: Some(Color::Reset),
|
||||
accent_color_override: None,
|
||||
accent_color_override: input_mode.accent_color(theme),
|
||||
border_color_override: None,
|
||||
prefix_override: None,
|
||||
placeholder_override: None,
|
||||
prefix_override: input_mode.prefix_override(theme),
|
||||
placeholder_override: input_mode.placeholder_override(multiline),
|
||||
show_accent_line: false,
|
||||
show_borders: false,
|
||||
title: None,
|
||||
|
|
@ -115,7 +122,11 @@ pub fn draw_live(app: &mut AppView, terminal: &mut PagerTerminal) {
|
|||
let theme = Theme::current();
|
||||
let commit_app = super::commit::committed_appearance(appearance);
|
||||
let compact = appearance.prompt.compact;
|
||||
let style = prompt_style(appearance);
|
||||
let (input_mode, multiline) = agent_id
|
||||
.and_then(|id| agents.get(&id))
|
||||
.map(|a| (a.prompt_input_mode, a.multiline_mode))
|
||||
.unwrap_or_default();
|
||||
let style = prompt_style(appearance, input_mode, &theme, multiline);
|
||||
let row_inset = live_left_inset(appearance);
|
||||
let layout_cfg = &appearance.scrollback.layout;
|
||||
let term_h = terminal.last_known_area().height;
|
||||
|
|
@ -550,24 +561,26 @@ fn render_minimal_status(
|
|||
turn_status::render_turn_status(
|
||||
buf,
|
||||
area,
|
||||
&agent.session.state,
|
||||
activity,
|
||||
agent.turn_elapsed(),
|
||||
agent.activity_started_at,
|
||||
agent.scrollback.animation_tick(),
|
||||
drain_blocked,
|
||||
None,
|
||||
false,
|
||||
agent.context_state.as_ref().map(|c| c.used),
|
||||
minimal_api::mcp_init_progress(agent),
|
||||
agent.bash_turn,
|
||||
is_pending_user_input,
|
||||
goal_verifying,
|
||||
watchers,
|
||||
parked,
|
||||
true,
|
||||
minimal_api::held_queue_count(agent),
|
||||
minimal_api::held_queue_top_sendable(agent),
|
||||
turn_status::TurnStatusArgs {
|
||||
state: &agent.session.state,
|
||||
activity,
|
||||
turn_elapsed: agent.turn_elapsed(),
|
||||
activity_started_at: agent.activity_started_at,
|
||||
tick: agent.scrollback.animation_tick(),
|
||||
drain_blocked,
|
||||
buttons: None,
|
||||
has_running_execute: false,
|
||||
total_tokens: agent.context_state.as_ref().map(|c| c.used),
|
||||
mcp_init_progress: minimal_api::mcp_init_progress(agent),
|
||||
is_bash_turn: agent.bash_turn,
|
||||
is_pending_user_input,
|
||||
goal_verifying,
|
||||
watchers,
|
||||
parked,
|
||||
flat_background: true,
|
||||
held_queue: minimal_api::held_queue_count(agent),
|
||||
held_queue_top_sendable: minimal_api::held_queue_top_sendable(agent),
|
||||
},
|
||||
);
|
||||
}
|
||||
/// Idle status: `minimal · [/fullscreen to go back ·] /help` (+ auto-set note).
|
||||
|
|
@ -615,41 +628,45 @@ fn render_prompt_info(
|
|||
let base = theme.primary().bg(Color::Reset);
|
||||
let sep = theme.dim().bg(Color::Reset);
|
||||
let mut segs: Vec<(String, Style)> = Vec::new();
|
||||
if let Some(model) = agent.session.models.current_model_name() {
|
||||
let label = match agent.session.models.reasoning_effort {
|
||||
Some(eff) => format!("{model} ({eff})"),
|
||||
None => model,
|
||||
};
|
||||
segs.push((label, base));
|
||||
}
|
||||
let effective_plan =
|
||||
minimal_api::plan_mode_pending(agent).unwrap_or(minimal_api::plan_mode_active(agent));
|
||||
let mode_flag: Option<(&str, Color)> = if effective_plan {
|
||||
Some(("plan", theme.accent_plan))
|
||||
} else if agent.session.is_yolo() {
|
||||
Some(("always-approve", theme.warning))
|
||||
} else if agent.session.is_auto() {
|
||||
Some(("auto", theme.accent_system))
|
||||
if let Some(label) = agent.prompt_input_mode.prompt_info_override() {
|
||||
segs.push((label.to_string(), base));
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((label, color)) = mode_flag {
|
||||
segs.push((label.to_string(), base.fg(color)));
|
||||
}
|
||||
let used = agent.context_state.as_ref().map(|c| c.used);
|
||||
let total = agent
|
||||
.context_state
|
||||
.as_ref()
|
||||
.and_then(|c| (c.total > 0).then_some(c.total))
|
||||
.or_else(|| agent.session.models.get_context_window());
|
||||
if let (Some(used), Some(total)) = (used, total)
|
||||
&& total > 0
|
||||
{
|
||||
let pct = xai_token_estimation::usage_percentage(used, total);
|
||||
segs.push((
|
||||
format!("{} / {} ({:.0}%)", fmt_tokens(used), fmt_tokens(total), pct),
|
||||
base,
|
||||
));
|
||||
if let Some(model) = agent.session.models.current_model_name() {
|
||||
let label = match agent.session.models.reasoning_effort {
|
||||
Some(eff) => format!("{model} ({eff})"),
|
||||
None => model,
|
||||
};
|
||||
segs.push((label, base));
|
||||
}
|
||||
let effective_plan =
|
||||
minimal_api::plan_mode_pending(agent).unwrap_or(minimal_api::plan_mode_active(agent));
|
||||
let mode_flag: Option<(&str, Color)> = if effective_plan {
|
||||
Some(("plan", theme.accent_plan))
|
||||
} else if agent.session.is_yolo() {
|
||||
Some(("always-approve", theme.warning))
|
||||
} else if agent.session.is_auto() {
|
||||
Some(("auto", theme.accent_system))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((label, color)) = mode_flag {
|
||||
segs.push((label.to_string(), base.fg(color)));
|
||||
}
|
||||
let used = agent.context_state.as_ref().map(|c| c.used);
|
||||
let total = agent
|
||||
.context_state
|
||||
.as_ref()
|
||||
.and_then(|c| (c.total > 0).then_some(c.total))
|
||||
.or_else(|| agent.session.models.get_context_window());
|
||||
if let (Some(used), Some(total)) = (used, total)
|
||||
&& total > 0
|
||||
{
|
||||
let pct = xai_token_estimation::usage_percentage(used, total);
|
||||
segs.push((
|
||||
format!("{} / {} ({:.0}%)", fmt_tokens(used), fmt_tokens(total), pct),
|
||||
base,
|
||||
));
|
||||
}
|
||||
}
|
||||
if queued > 0 {
|
||||
segs.push((format!("{queued} queued"), base));
|
||||
|
|
@ -890,6 +907,28 @@ mod tests {
|
|||
assert!(!text.contains("/help"), "not the idle hint: {text:?}");
|
||||
}
|
||||
#[test]
|
||||
fn prompt_style_bash_mode_shows_bang_prefix() {
|
||||
use xai_grok_pager::app::agent_view::PromptInputMode;
|
||||
use xai_grok_pager::appearance::AppearanceConfig;
|
||||
let appearance = AppearanceConfig::default();
|
||||
let theme = Theme::current();
|
||||
let normal = prompt_style(&appearance, PromptInputMode::Normal, &theme, false);
|
||||
assert!(normal.prefix_override.is_none());
|
||||
assert!(normal.accent_color_override.is_none());
|
||||
assert!(normal.placeholder_override.is_none());
|
||||
let bash = prompt_style(&appearance, PromptInputMode::Bash, &theme, false);
|
||||
assert_eq!(
|
||||
bash.prefix_override,
|
||||
Some(("! ", theme.command)),
|
||||
"bash mode must paint the yellow `! ` prefix (full-TUI parity)"
|
||||
);
|
||||
assert_eq!(bash.accent_color_override, Some(theme.command));
|
||||
assert!(
|
||||
bash.placeholder_override.is_none(),
|
||||
"bash keeps the default placeholder"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn prompt_info_renders_model_context_and_queued() {
|
||||
let mut a = agent();
|
||||
a.context_state = Some(xai_grok_shell::session::ContextInfo {
|
||||
|
|
@ -913,6 +952,37 @@ mod tests {
|
|||
"trailing transcript hint: {text:?}"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn prompt_info_bash_mode_shows_run_shell_command() {
|
||||
use xai_grok_pager::app::agent_view::PromptInputMode;
|
||||
let mut a = agent();
|
||||
a.prompt_input_mode = PromptInputMode::Bash;
|
||||
a.context_state = Some(xai_grok_shell::session::ContextInfo {
|
||||
used: 276_000,
|
||||
total: 2_000_000,
|
||||
..Default::default()
|
||||
});
|
||||
let theme = Theme::current();
|
||||
let area = Rect::new(0, 0, 80, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
render_prompt_info(&mut buf, area, &a, 2, "ctrl+o transcript", &theme);
|
||||
let text: String = (0..area.width)
|
||||
.filter_map(|x| buf.cell((x, 0)).map(|c| c.symbol().to_string()))
|
||||
.collect();
|
||||
assert!(
|
||||
text.contains("Run shell command"),
|
||||
"bash mode info label: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("276K"),
|
||||
"context usage hidden under bash mode: {text:?}"
|
||||
);
|
||||
assert!(text.contains("2 queued"), "queued still shown: {text:?}");
|
||||
assert!(
|
||||
text.trim_end().ends_with("ctrl+o transcript"),
|
||||
"transcript hint still trails: {text:?}"
|
||||
);
|
||||
}
|
||||
/// Where Ctrl+O is the interject chord (Apple Terminal) the caller passes
|
||||
/// the `/transcript` fallback, and the info row advertises that instead.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -218,11 +218,11 @@ fn compute_target(app: &mut AppView, term_h: u16, width: u16) -> u16 {
|
|||
// Ctrl+T "force-show" pin; effective visibility (auto-hide) is computed per
|
||||
// agent by `todo_panel_height`.
|
||||
let force_todos = minimal_api::minimal_show_todos(app);
|
||||
// Snapshot appearance-derived inputs before borrowing `agents` mutably.
|
||||
let style = super::live::prompt_style(&app.appearance);
|
||||
// Committed appearance (timestamps off) so the measured tail height matches
|
||||
// exactly what `draw_tail` renders.
|
||||
let commit_app = super::commit::committed_appearance(&app.appearance);
|
||||
// Theme + prompt style: built after the agent is known so bash/feedback/
|
||||
// remember chrome matches `draw_live` (same `prompt_style` inputs).
|
||||
// Minimal is flush-left (W-38): prompt-replacing modals span the live
|
||||
// region's full width (no outer horizontal padding), so measure their
|
||||
// height at that same width — it must match `live::draw_live`'s
|
||||
|
|
@ -242,6 +242,16 @@ fn compute_target(app: &mut AppView, term_h: u16, width: u16) -> u16 {
|
|||
return needed.max(base).min(ceiling);
|
||||
};
|
||||
let id = *id;
|
||||
// Snapshot mode/multiline before the mut agent borrow so `prompt_style` can
|
||||
// still read `app.appearance` (same inputs as `draw_live`).
|
||||
let (input_mode, multiline) = app
|
||||
.agents
|
||||
.get(&id)
|
||||
.map(|a| (a.prompt_input_mode, a.multiline_mode))
|
||||
.unwrap_or_default();
|
||||
let theme = xai_grok_pager::theme::Theme::current();
|
||||
let style = super::live::prompt_style(&app.appearance, input_mode, &theme, multiline);
|
||||
|
||||
let Some(agent) = app.agents.get_mut(&id) else {
|
||||
return base;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue