Synced from monorepo

Changes:
- Non-blocking coding-data sharing upsell banner
- Consolidate remediation in Doctor
- Auto mode defers fail-closed gate asks to the classifier
- Coalesce marketplace list fetches
- Allow removing a marketplace source by name
- Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal)
- Label failed workspace RPCs with error_kind
- Drop redundant explicit tonic/prost deps from xai-grok-shell
- Report real exit codes for completed background shells
- Narrow the date-rollover reminder to date-bearing templates
- Wire toolOverrides through the session and agent
- Security: Bash(git:*) allowlist matches whole command chain by prefix
- Split prompt-trigger telemetry and record classifier provenance
- Raise connectors-manager timeout to 60s
- Auto classifier honors recorded approvals for repeat actions
- Apply doctor fixes in the TUI
- Auto-mode classifier timeouts prompt instead of silently denying
- Scope subagent completion drains to the owning session
- Add the toolOverrides wire types
- Set client_identifier=grok-agent-sdk
- Accept both spellings of the workspace-teleport kill switch
- Persist one-shot occurrence journal
- Stop turns that poll the exact same tool call 16x in a row
- Copy compaction checkpoint files when forking sessions
- Auto-focus permission prompt from scrollback
- Esc cancels the running turn in non-vim and minimal modes
- List Ctrl+Z undo and redo in keyboard shortcuts
- Out-of-process macOS mic capture
- Show active auth mode on session-info
- Install the npm binary under $GROK_HOME
- Remove hover/click dead zones between dashboard items
- Route startup warnings to doctor
- Document [feedback.user] author identity config
- Extend bang command timeout
- Close combine-queued edit-hold race
- Integrate relocation recovery
- Expose privacy notice rollout flag
- Break harness discovery ref cycle so connections can idle-evict
- Shift/Alt+Enter inserts newline when editing a queued prompt
- Gate project Claude permissions on folder trust
- Echo response.create.event_id on response.created
- Toast when session creation fails from disk full
- Add shared test process lifecycle
- Enable dynamic workflows by default
- Add relocation transaction state machine
- Add shared test sandbox
- Surface auth failures on model-switch compact
- Persist durable scheduler expiry
- Confirm before removing extensions-modal items
- Re-run compact and prompt after login when compact hit expired auth
- Recap sends hosted tools under backend search
This commit is contained in:
grokkybara[bot] 2026-07-22 19:18:53 +01:00
commit a5727c5960
482 changed files with 37627 additions and 13402 deletions

View file

@ -52,31 +52,33 @@ pub enum PromptMode {
impl AgentView {
/// Editing-mode key intercepts for the prompt pane.
///
/// Bare Enter saves, Esc cancels.
/// Interject-key handling for edit mode lives in
/// `interject_editing_queued_intercept`, reached via the
/// `ActionId::InterjectPrompt` registry arm in `handle_prompt_key` (the
/// binding is remappable, so it cannot be matched on a raw key here).
/// Shift-Enter / Alt-Enter fall through to widget (newline insertion).
/// Tab removed as cancel trigger — too easy to hit accidentally.
/// Ctrl-C on empty prompt also cancels (matches cancel-turn pattern).
/// Bare Enter saves, Esc (or Ctrl-C on empty) cancels. Shift/Alt+Enter
/// inserts a newline (same as the normal composer) and must not save.
/// Apple Terminal Cmd/Shift/Opt+Enter is rescued inside `is_mod_enter`
/// via CoreGraphics — not a universal Cmd+Enter binding.
/// Interject is remappable, so it is handled via the
/// `ActionId::InterjectPrompt` registry arm → `interject_editing_queued_intercept`,
/// not matched as a raw key here.
///
/// Returns `None` when not editing or for any unhandled key — the key
/// MUST fall through to the widget (typing, newline insertion).
/// Returns `None` when not editing or unhandled — must fall through to the widget.
pub(super) fn handle_editing_queued_key(&mut self, key: &KeyEvent) -> Option<InputOutcome> {
if let PromptMode::EditingQueued { id, server_id, .. } = &self.prompt_mode {
let (id, server_id) = (*id, server_id.clone());
let ctrl_c_empty = key!('c', CONTROL).matches(key) && self.prompt.text().is_empty();
// Before bare-Enter save: Shift/Alt flags, or Apple Terminal bare
// Enter with Cmd/Shift/Opt held (CoreGraphics rescue in is_mod_enter).
if crate::input::is_mod_enter(key) {
self.prompt.textarea.insert_str("\n");
return Some(InputOutcome::Changed);
}
if key!(Enter).matches(key) && !self.prompt.text().trim().is_empty() {
return Some(self.save_edited_queued_row(id, server_id, true));
}
if key.code == KeyCode::Esc || ctrl_c_empty {
// Discard changes.
self.exit_editing_mode();
return Some(InputOutcome::Action(Action::DrainQueue));
}
// Everything else (including Shift-Enter, Alt-Enter, typing) falls through.
}
None
}
@ -319,10 +321,11 @@ impl AgentView {
match server_id {
Some(server_id) => {
let new_text = self.prompt.text().to_string();
// Server-origin row: route the edit through the agent (LWW).
// The rebroadcast updates every client's shared queue mirror
// — do NOT mutate locally.
self.exit_editing_mode();
// Server-origin row: route the edit through the agent (LWW); the
// rebroadcast updates every client's mirror, so don't mutate
// locally. Keep the hold until the edit lands — see
// `exit_editing_mode_keeping_hold`.
self.exit_editing_mode_keeping_hold();
InputOutcome::Action(Action::QueueEditShared {
id: server_id,
new_text,
@ -427,7 +430,9 @@ impl AgentView {
self.show_toast("Images can't be attached when editing a shared queued prompt");
}
// new_text carries the edit — without it the agent would
// interject the original server-side text.
// interject the original server-side text. Release is safe
// here: interject removes the row from the queue (no
// combine-on-stale-text window for a still-queued hold).
let expected_version = self.queue.row_ref(id).map(|r| r.version);
self.exit_editing_mode();
match expected_version {
@ -496,20 +501,34 @@ impl AgentView {
}
/// Exit editing mode: restore stashed text, clear mode, focus queue pane.
/// No-op unless `EditingQueued`.
/// No-op unless `EditingQueued`. The default exit; releases the
/// server-side combine hold (cancel, lost-row, interject, modal paths).
///
/// Always resets `prompt_input_mode` to `Normal` so it doesn't leak
/// into subsequent normal prompt entry.
pub(super) fn exit_editing_mode(&mut self) {
self.exit_editing_mode_inner(true);
}
/// Exit editing without emitting `QueueReleaseEdit` — the server-row save
/// path's `QueueEditShared` clears the hold on the shell instead. Releasing
/// here would flush first (via `pending_effects`) and let combine merge the
/// row on stale text before the edit lands.
fn exit_editing_mode_keeping_hold(&mut self) {
self.exit_editing_mode_inner(false);
}
fn exit_editing_mode_inner(&mut self, release_hold: bool) {
// Idempotent: remove_local_queue_row's guard may have exited already;
// a second take() of the spent stash would wipe the composer.
if !matches!(self.prompt_mode, PromptMode::EditingQueued { .. }) {
return;
}
if let PromptMode::EditingQueued {
server_id: Some(sid),
..
} = &self.prompt_mode
if release_hold
&& let PromptMode::EditingQueued {
server_id: Some(sid),
..
} = &self.prompt_mode
&& let Some(session_id) = self.session.session_id.clone()
{
self.pending_effects
@ -573,6 +592,62 @@ mod tests {
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)
}
fn enter_edit_local_row() -> AgentView {
let mut agent = make_running_agent();
let registry = non_vscode_registry();
let ids = agent.queue.entry_ids();
// Local row is second (server rendered first).
agent.queue.list_state.select_by_id(ids[1]);
let _ = agent.handle_queue_key(&edit_key(), &registry);
assert!(matches!(
agent.prompt_mode,
PromptMode::EditingQueued { .. }
));
agent
}
/// Shift/Alt+Enter → newline in edit mode (must not save).
/// Cmd/SUPER is not a product-wide newline chord (Apple Terminal only via CG).
#[test]
fn edit_mod_enter_inserts_newline_without_exiting() {
for mods in [KeyModifiers::SHIFT, KeyModifiers::ALT] {
let mut agent = enter_edit_local_row();
agent.prompt.set_text("line1");
let outcome = agent.handle_prompt_key_for_test(&KeyEvent::new(KeyCode::Enter, mods));
assert!(
matches!(outcome, InputOutcome::Changed),
"mod Enter ({mods:?}) must not save; got {outcome:?}"
);
assert!(
matches!(agent.prompt_mode, PromptMode::EditingQueued { .. }),
"must stay in edit mode for {mods:?}"
);
assert_eq!(
agent.prompt.text(),
"line1\n",
"mod Enter ({mods:?}) must insert a newline"
);
assert_eq!(
agent.session.pending_prompts[0].text, "local one",
"queue row must stay unchanged for {mods:?}"
);
}
}
/// Bare Enter still saves (mod-enter path must not steal it).
#[test]
fn edit_bare_enter_still_saves() {
let mut agent = enter_edit_local_row();
agent.prompt.set_text("line1 EDITED");
let outcome = agent.handle_prompt_key_for_test(&enter_key());
assert!(
matches!(outcome, InputOutcome::Action(Action::DrainQueue)),
"bare Enter must save; got {outcome:?}"
);
assert!(matches!(agent.prompt_mode, PromptMode::Normal));
assert_eq!(agent.session.pending_prompts[0].text, "line1 EDITED");
}
fn attach_image_to_local_row(agent: &mut AgentView) {
let mut image = test_pasted_image();
image.display_number = 1;
@ -648,6 +723,67 @@ mod tests {
assert!(matches!(agent.prompt_mode, PromptMode::Normal));
}
/// Saving a server-row edit must not emit `QueueReleaseEdit` — see
/// `exit_editing_mode_keeping_hold`.
#[test]
fn submit_server_edit_keeps_combine_hold_until_edit() {
use crate::app::actions::Effect;
let mut agent = make_running_agent();
let registry = non_vscode_registry();
let ids = agent.queue.entry_ids();
agent.queue.list_state.select_by_id(ids[0]);
let _ = agent.handle_queue_key(&edit_key(), &registry);
// Entering edit on a server row arms the hold.
assert!(
agent
.pending_effects
.iter()
.any(|e| matches!(e, Effect::QueueHoldEdit { .. })),
"entering edit must emit QueueHoldEdit"
);
agent.prompt.set_text("server one EDITED");
let outcome = agent.handle_prompt_key_for_test(&enter_key());
assert!(
matches!(
outcome,
InputOutcome::Action(Action::QueueEditShared { .. })
),
"save must route to QueueEditShared"
);
assert!(
!agent
.pending_effects
.iter()
.any(|e| matches!(e, Effect::QueueReleaseEdit { .. })),
"server-row save must NOT emit QueueReleaseEdit (the edit clears the hold)"
);
}
/// Cancelling (Esc) a server-row edit still releases the hold, so an
/// abandoned edit can't pin the row out of combine.
#[test]
fn cancel_server_edit_releases_combine_hold() {
use crate::app::actions::Effect;
let mut agent = make_running_agent();
let registry = non_vscode_registry();
let ids = agent.queue.entry_ids();
agent.queue.list_state.select_by_id(ids[0]);
let _ = agent.handle_queue_key(&edit_key(), &registry);
agent.pending_effects.clear();
let _ = agent.handle_prompt_key_for_test(&KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(
agent
.pending_effects
.iter()
.any(|e| matches!(e, Effect::QueueReleaseEdit { .. })),
"cancelling an edit must emit QueueReleaseEdit"
);
}
#[test]
fn shared_queue_edit_rejects_image_before_normal_save() {
let mut agent = make_running_agent();