Synced from monorepo

Synced from monorepo

Changes:
- Workspace server: report `/ready` as failed with dwell on hub connect failure
- Refresh OIDC token for the Grok agent in the shell
- ACP terminal output recorder
- Cross-platform provider auth commands in the shell
- Default `/resume` to Grok sessions with a hint for hidden external sessions
- Resume sessions by title with `--resume`
- Limit app-builder archive size
- Data-driven tag labels for slash commands
- Doctor fixes for tmux
- Custom provider gateways and subprocess environment policy in the shell
- `/tutorial` — opt-in onboarding tour of Grok Build
- Soft and required CLI version checks in the shell
- Privacy banner env overrides survive live settings updates
- Add remote flag to override the image-edit model
- Return profile fields from auth info even when the access token is expired
- Add edit control on queued prompt rows
- Keep fail-closed policy when clearing orphans with no team
- Setting to disable the Ctrl+Space/F8 voice shortcut
- Pass `--raw` to pw-record so Linux dictation works on older PipeWire
- Validate git URLs when adding marketplace entries
- Stop shipping stale tool-doc parameter and tool names
- Re-point dashboard attach after `/fork` only when the parent was attached
- Surface Grok Computer media-generation results as file-path chunks
- Clear web background-task tray on kill and keep the task description
- Show privacy upsell banner in agent view until acted on
- Add tools-server client callback surface
- Protect persistent global hook sources

Source-Revision: 95d84f443eddcbed6cbfd6eed22e2eafe6b3939d
This commit is contained in:
grokkybara[bot] 2026-07-23 17:12:33 +00:00
commit 69f0ba880a
286 changed files with 22939 additions and 9624 deletions

View file

@ -9,8 +9,8 @@
use super::actions::Action;
use super::agent_view::{
AgentPane, AgentView, CONTEXT_CLICK_DEBOUNCE_MS, CtaPhase, MULTI_CLICK_TIMEOUT_MS,
PromptInputMode, TextClickState, app_should_open_link_on_click, has_native_link_hover,
is_link_modifier_held, is_text_selection_on_double_click,
PromptInputMode, PromptMode, TextClickState, app_should_open_link_on_click,
has_native_link_hover, is_link_modifier_held, is_text_selection_on_double_click,
};
use super::app_view::InputOutcome;
use crate::scrollback::block::BlockContent;
@ -125,6 +125,32 @@ impl AgentView {
self.cancel_trigger_hint = Some(crate::app::actions::CancelTrigger::Mouse);
return InputOutcome::Action(Action::CancelTurn);
}
if self
.privacy_banner
.hit_accept
.contains(mouse.column, mouse.row)
&& !self.pos_occluded(mouse.column, mouse.row)
{
return InputOutcome::Action(Action::PrivacyBannerAccept);
}
if self
.privacy_banner
.hit_customize
.contains(mouse.column, mouse.row)
&& !self.pos_occluded(mouse.column, mouse.row)
{
return InputOutcome::Action(Action::PrivacyBannerCustomize);
}
if self
.privacy_banner
.hit_legal
.contains(mouse.column, mouse.row)
&& !self.pos_occluded(mouse.column, mouse.row)
{
return InputOutcome::Action(Action::OpenUrl(
crate::views::privacy_banner::PRIVACY_BANNER_LEGAL_URL.to_string(),
));
}
if self.hit_announcement_hide.contains(mouse.column, mouse.row)
&& !self.pos_occluded(mouse.column, mouse.row)
{
@ -433,6 +459,18 @@ impl AgentView {
{
return InputOutcome::Action(action);
}
if let Some(id) = self.queue.edit_click(mouse.column, mouse.row)
&& (!matches!(self.prompt_mode, PromptMode::EditingQueued { .. })
|| self.set_active_pane(AgentPane::Queue, false))
{
let row = self.queue.row_ref(id);
let is_server = matches!(
row.as_ref().map(|r| r.origin),
Some(crate::views::queue_pane::QueueRowOrigin::Server)
);
self.enter_queue_edit(id, is_server, row);
return InputOutcome::Changed;
}
self.set_active_pane(AgentPane::Queue, false);
self.queue.handle_mouse(
mouse.kind,
@ -1013,13 +1051,12 @@ impl AgentView {
) {
changed |= self.queue.update_delete_hover(mouse.column, mouse.row);
changed |= self.queue.update_send_now_hover(mouse.column, mouse.row);
changed |= self.queue.update_edit_hover(mouse.column, mouse.row);
changed |= self.queue.update_row_hover(mouse.column, mouse.row);
} else {
if self.queue.hovered_delete_id.is_some() {
self.queue.clear_delete_hover();
changed = true;
}
changed |= self.queue.clear_delete_hover();
changed |= self.queue.clear_send_now_hover();
changed |= self.queue.clear_edit_hover();
changed |= self.queue.clear_row_hover();
}
changed |= self.hit_plan_button.update_hover(mouse.column, mouse.row);
@ -1037,6 +1074,18 @@ impl AgentView {
changed |= self
.hit_announcement_cta
.update_hover(mouse.column, mouse.row);
changed |= self
.privacy_banner
.hit_accept
.update_hover(mouse.column, mouse.row);
changed |= self
.privacy_banner
.hit_customize
.update_hover(mouse.column, mouse.row);
changed |= self
.privacy_banner
.hit_legal
.update_hover(mouse.column, mouse.row);
changed |= self
.plugin_cta
.hit_connect
@ -1252,6 +1301,10 @@ mod tests {
fn click_delete(agent: &mut AgentView, selected_id: u64) -> InputOutcome {
click_queue_button(agent, selected_id, |a, c, r| a.queue.delete_click(c, r))
}
/// Left-click the row's `[edit]` button.
fn click_edit(agent: &mut AgentView, selected_id: u64) -> InputOutcome {
click_queue_button(agent, selected_id, |a, c, r| a.queue.edit_click(c, r))
}
/// Mouse "Send now" (interject) on the last local row keeps the pane open
/// when a server row remains — the third sibling site of the same fix.
#[test]
@ -1377,6 +1430,190 @@ mod tests {
assert!(agent.active_modal.is_none());
assert_eq!(agent.prompt.text(), "draft");
}
/// Mouse `[edit]` on a queued row enters the same queued-edit flow as the
/// keyboard `e` (`QueueEvent::EditSelected` → `enter_queue_edit`): the
/// composer loads the row text and the prompt pane takes focus in
/// `EditingQueued` mode, leaving the row itself queued.
#[test]
fn mouse_edit_click_enters_queued_edit_mode() {
let mut agent = running_agent_local_only();
let ids = agent.queue.entry_ids();
let outcome = click_edit(&mut agent, ids[0]);
assert!(
matches!(outcome, InputOutcome::Changed),
"edit click redraws without dispatching an action, got {outcome:?}"
);
match &agent.prompt_mode {
PromptMode::EditingQueued {
id,
original,
server_id,
..
} => {
assert_eq!(*id, ids[0]);
assert_eq!(original, "local one");
assert!(
server_id.is_none(),
"local row must not take the server edit path"
);
}
other => panic!("expected EditingQueued, got {other:?}"),
}
assert_eq!(agent.prompt.text(), "local one");
assert_eq!(agent.active_pane, AgentPane::Prompt);
assert_eq!(agent.session.pending_prompts.len(), 1);
}
/// Clicking another row's `[edit]` while a DIRTY queued edit is active
/// must not re-enter `enter_queue_edit` — that would bypass the
/// dirty-edit lock and overwrite `stashed_prompt`, so Esc would restore
/// the edit text instead of the user's original draft. The click falls
/// through to the pane switch, which the lock blocks.
#[test]
fn mouse_edit_click_during_dirty_edit_preserves_first_edit_and_stash() {
let mut agent = make_running_agent();
let ids = agent.queue.entry_ids();
agent.stashed_prompt = Some(crate::views::prompt_widget::StashedPrompt {
text: "draft".into(),
cursor: 0,
images: Vec::new(),
chip_elements: Vec::new(),
image_counter: 0,
image_undo_stash: Vec::new(),
});
agent.prompt_mode = PromptMode::EditingQueued {
id: ids[1],
original: "local one".into(),
server_id: None,
kind: crate::app::agent::QueueEntryKind::Prompt,
};
agent.prompt.set_text("local one EDITED");
agent.active_pane = AgentPane::Prompt;
let outcome = click_edit(&mut agent, ids[0]);
assert!(matches!(outcome, InputOutcome::Changed), "got {outcome:?}");
match &agent.prompt_mode {
PromptMode::EditingQueued { id, original, .. } => {
assert_eq!(*id, ids[1], "the first edit's target row must survive");
assert_eq!(original, "local one");
}
other => panic!("expected the first edit to stay active, got {other:?}"),
}
assert_eq!(agent.prompt.text(), "local one EDITED");
assert_eq!(
agent.stashed_prompt.as_ref().map(|s| s.text.as_str()),
Some("draft"),
"the pre-edit draft must survive for Esc-restore"
);
assert!(
agent.pending_effects.is_empty(),
"no hold effect may be emitted for the clicked row"
);
}
/// Same guard for a dirty SERVER-row edit: clicking another row's
/// `[edit]` must not replace the edit (which would strand the first
/// row's combine hold) nor emit a second `QueueHoldEdit`.
#[test]
fn mouse_edit_click_during_dirty_server_edit_keeps_hold_target() {
let mut agent = make_running_agent();
let ids = agent.queue.entry_ids();
agent.prompt_mode = PromptMode::EditingQueued {
id: ids[0],
original: "server one".into(),
server_id: Some("p1".into()),
kind: crate::app::agent::QueueEntryKind::Prompt,
};
agent.prompt.set_text("server one EDITED");
agent.active_pane = AgentPane::Prompt;
let outcome = click_edit(&mut agent, ids[1]);
assert!(matches!(outcome, InputOutcome::Changed), "got {outcome:?}");
match &agent.prompt_mode {
PromptMode::EditingQueued { id, server_id, .. } => {
assert_eq!(*id, ids[0], "the held server row must stay the edit target");
assert_eq!(server_id.as_deref(), Some("p1"));
}
other => panic!("expected the server edit to stay active, got {other:?}"),
}
assert_eq!(agent.prompt.text(), "server one EDITED");
assert!(
agent.pending_effects.is_empty(),
"no second QueueHoldEdit may be emitted while one row is held"
);
}
/// Clicking another row's `[edit]` while a CLEAN (unchanged) edit is
/// active must open the clicked row's edit on the SAME click: the
/// canonical pane switch exits the clean edit — releasing its server
/// combine hold — and the arm then enters the clicked row instead of
/// letting the click die on the pane switch.
#[test]
fn mouse_edit_click_during_clean_edit_switches_to_clicked_row() {
let mut agent = make_running_agent();
let ids = agent.queue.entry_ids();
agent.stashed_prompt = Some(crate::views::prompt_widget::StashedPrompt {
text: "draft".into(),
cursor: 0,
images: Vec::new(),
chip_elements: Vec::new(),
image_counter: 0,
image_undo_stash: Vec::new(),
});
agent.prompt_mode = PromptMode::EditingQueued {
id: ids[0],
original: "server one".into(),
server_id: Some("p1".into()),
kind: crate::app::agent::QueueEntryKind::Prompt,
};
agent.prompt.set_text("server one");
agent.active_pane = AgentPane::Prompt;
let outcome = click_edit(&mut agent, ids[1]);
assert!(matches!(outcome, InputOutcome::Changed), "got {outcome:?}");
match &agent.prompt_mode {
PromptMode::EditingQueued {
id,
original,
server_id,
..
} => {
assert_eq!(*id, ids[1], "one click must open the clicked row's edit");
assert_eq!(original, "local one");
assert!(server_id.is_none());
}
other => panic!("expected EditingQueued for the clicked row, got {other:?}"),
}
assert_eq!(agent.prompt.text(), "local one");
assert_eq!(agent.active_pane, AgentPane::Prompt);
assert!(
agent.pending_effects.iter().any(|e| matches!(
e,
crate::app::actions::Effect::QueueReleaseEdit { id, .. } if id == "p1"
)),
"clean exit must release the held server row, effects = {:?}",
agent.pending_effects
);
assert_eq!(
agent.stashed_prompt.as_ref().map(|s| s.text.as_str()),
Some("draft")
);
}
/// Clicking `[edit]` on a server row still awaiting its enqueue
/// confirmation (an optimistic echo) is ignored: the shell has no row to
/// hold yet, so the `hold_edit` would no-op and the later-confirmed row
/// could be absorbed mid-edit.
#[test]
fn mouse_edit_click_on_optimistic_server_row_is_ignored() {
let mut agent = make_running_agent();
agent.optimistic_queue_ids.insert("p1".into());
let ids = agent.queue.entry_ids();
let outcome = click_edit(&mut agent, ids[0]);
assert!(matches!(outcome, InputOutcome::Changed), "got {outcome:?}");
assert!(
matches!(agent.prompt_mode, PromptMode::Normal),
"an unconfirmed echo must not be editable"
);
assert_eq!(agent.prompt.text(), "");
assert!(
agent.pending_effects.is_empty(),
"no QueueHoldEdit may be emitted for a row the shell doesn't have"
);
}
/// A synthetic left-click on a rendered follow-up chip yields the LITERAL
/// `SubmitFollowUp` action (never a slash-command path).
#[test]