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:
parent
a5727c5960
commit
69f0ba880a
286 changed files with 22939 additions and 9624 deletions
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue