F3: Kimi inference pipeline + full grok cloud-surface excision
Sampler / inference (PRD F3):
- kimi_compat.rs: single adaptation point for the Kimi chat/completions
dialect (thinking-field mapping, model_id stripping, empty-content
tool-call message fix, stream_options.include_usage), with kimi-cli
source citations
- Rate-limit handling reworked for Kimi/Moonshot semantics; UA kigi/{version}
- /models replaces the xAI models-v2 endpoint everywhere; idle model
refresh carries X-Msh-* device headers only (X-XAI-Token-Auth and
x-grok-client-mode/CLIENT_MODE_HEADER machinery deleted)
Cloud-surface excision (PRD §5, zero-egress):
- remote/ conversations lane, cli-chat-proxy-types crate, prod/ dir,
share command, credit bar: deleted (single local session lane;
paginate() replaces merge_and_paginate)
- Subscription/tier gate stack deleted end-to-end: AppView
gate/tier/team/ZDR fields, app/subscription.rs watch loop,
dispatch/billing.rs paywall + SuperGrok upsell, free-usage-exhausted
chain, tier-restricted commands, GateInfo, RemoteSettings gate fields,
SettingsUpdateNotification gate fields
- /privacy + coding-data-sharing setting deleted (backed by a dead xAI
RPC; Kigi is zero-egress — nothing to share or retain remotely)
Auth UX correctness (user-reported):
- Device-flow fixtures now mirror the live Kimi payload shape
(https://www.kimi.com/code/authorize_device?user_code=..., verified
against auth.kimi.com); the fabricated auth.kimi.com/device?code=...
URLs are gone
- open_browser_detached is a no-op under cfg(test): unit tests drove
wiremock fixture URLs into the real browser (root cause of the
"garbage mock link" ABCD-1234 tabs)
- Welcome/pager-minimal rebrand: Grok Build -> Kigi, grok.com ->
kimi.com, "Sign in to Grok" -> "Sign in to Kimi"
This commit is contained in:
1 parent
fe1f885bb3
commit
ea0ce9d15f
231 files changed
+4730
-26358
No files matched your search
@@ -80,8 +80,6 @@ pub(crate) fn handle_ask_user_question(
|
||||
let cmd = match kind {
|
||||
LocalQuestionKind::Fork { .. } => "/fork",
|
||||
LocalQuestionKind::NewSession => "/new",
|
||||
LocalQuestionKind::CreditLimitUpsell => "credit-limit upsell",
|
||||
LocalQuestionKind::FreeUsageUpsell => "SuperGrok upsell",
|
||||
LocalQuestionKind::AgentTypeMismatch { .. } => "model switch",
|
||||
LocalQuestionKind::ProjectSelect { .. } => "project select",
|
||||
};
|
||||
|
||||
@@ -67,8 +67,6 @@ use prompt_origin::{push_wake_end_marker, viewer_turn_anchor, wake_turn_elapsed}
|
||||
pub(crate) use subagent_activity::finalize_killed_subagent;
|
||||
use subagent_activity::{subagent_activity_label, sync_subagent_activity};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use session_notification::apply_session_event_for_test;
|
||||
use session_notification::{
|
||||
advance_reconnect_cursor, confirm_context_used, detect_plan_mode_change,
|
||||
drop_unexpected_replay, handle_session_notification,
|
||||
|
||||
@@ -342,8 +342,6 @@ pub(super) fn handle_session_notification(notif: &acp::ExtNotification, app: &mu
|
||||
restore_degree: None,
|
||||
rate_limited: false,
|
||||
model_incompatible: false,
|
||||
credit_limit_blocked: false,
|
||||
free_usage_blocked: false,
|
||||
bg_tasks: std::collections::BTreeMap::new(),
|
||||
bg_tool_call_to_task: std::collections::HashMap::new(),
|
||||
scheduled_tasks: std::collections::HashMap::new(),
|
||||
@@ -363,7 +361,6 @@ pub(super) fn handle_session_notification(notif: &acp::ExtNotification, app: &mu
|
||||
child_view.set_input_mode(InputMode::Vim);
|
||||
child_view.is_subagent_view = true;
|
||||
child_view.active_pane = crate::views::agent::ActivePane::Scrollback;
|
||||
child_view.set_sharing_enabled(agent.sharing_enabled);
|
||||
let usage_visible = agent
|
||||
.prompt
|
||||
.slash_controller
|
||||
@@ -1087,18 +1084,6 @@ pub(super) fn handle_child_session_notification(
|
||||
/// Apply a compaction or retry event to a session's activity state and scrollback.
|
||||
///
|
||||
/// Shared between the root agent and child (subagent) notification paths.
|
||||
/// Test-only shim so dispatch-level tests can replay real notification
|
||||
/// sequences (e.g. `RetryState::Retrying` → `Exhausted`) through the
|
||||
/// production handler — the Retrying arm clears the `in_flight_prompt`
|
||||
/// rewind stash, which a fixture setting fields directly would miss.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn apply_session_event_for_test(
|
||||
update: &XaiSessionUpdate,
|
||||
session: &mut AgentSession,
|
||||
scrollback: &mut crate::scrollback::state::ScrollbackState,
|
||||
) -> bool {
|
||||
apply_session_event(update, session, scrollback, false)
|
||||
}
|
||||
pub(super) fn apply_session_event(
|
||||
update: &XaiSessionUpdate,
|
||||
session: &mut AgentSession,
|
||||
@@ -1215,7 +1200,6 @@ pub(super) fn apply_retry_state(
|
||||
scrollback: &mut crate::scrollback::state::ScrollbackState,
|
||||
is_api_key_auth: bool,
|
||||
) {
|
||||
let mut is_credit_limit = false;
|
||||
let mut is_reauth = false;
|
||||
use kigi_shell::extensions::notification::RetryState;
|
||||
match retry {
|
||||
@@ -1238,14 +1222,7 @@ pub(super) fn apply_retry_state(
|
||||
session.set_retry_activity(None);
|
||||
session.rate_limited = *rate_limited;
|
||||
|
||||
is_credit_limit = super::super::dispatch::is_credit_limit_error(None, reason);
|
||||
let is_free_usage =
|
||||
*rate_limited && super::super::dispatch::is_free_usage_exhausted_error(reason);
|
||||
if is_credit_limit {
|
||||
session.credit_limit_blocked = true;
|
||||
} else if is_free_usage {
|
||||
session.free_usage_blocked = true;
|
||||
} else if !*rate_limited && is_reauthable_failure(None, reason) {
|
||||
if !*rate_limited && is_reauthable_failure(None, reason) {
|
||||
is_reauth = true;
|
||||
scrollback.push_block(RenderBlock::session_event(SessionEvent::ReAuthRequired));
|
||||
} else {
|
||||
@@ -1268,10 +1245,7 @@ pub(super) fn apply_retry_state(
|
||||
if error_type == "encrypted_content_mismatch" {
|
||||
session.model_incompatible = true;
|
||||
}
|
||||
is_credit_limit = super::super::dispatch::is_credit_limit_error(None, message);
|
||||
if is_credit_limit {
|
||||
session.credit_limit_blocked = true;
|
||||
} else if is_reauthable_failure(Some(error_type.as_str()), message) {
|
||||
if is_reauthable_failure(Some(error_type.as_str()), message) {
|
||||
is_reauth = true;
|
||||
scrollback.push_block(RenderBlock::session_event(SessionEvent::ReAuthRequired));
|
||||
} else if error_type == "context_length" {
|
||||
@@ -1287,8 +1261,7 @@ pub(super) fn apply_retry_state(
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_credit_limit {
|
||||
} else if !is_reauth {
|
||||
if !is_reauth {
|
||||
session.in_flight_prompt = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,23 +107,6 @@ pub(super) fn handle_settings_update(notif: &acp::ExtNotification, app: &mut App
|
||||
if let Some(v) = update.show_resolved_model {
|
||||
app.show_resolved_model = v;
|
||||
}
|
||||
if let Some(v) = update.sharing_enabled {
|
||||
app.sharing_enabled = v;
|
||||
// Propagate to existing agents so slash-command registries stay
|
||||
// in sync (same fan-out pattern used when creating new agents).
|
||||
for agent in app.agents.values_mut() {
|
||||
agent.set_sharing_enabled(v);
|
||||
}
|
||||
}
|
||||
// Always recompute is_api_key_auth from the tier so a later Free/SuperGrok
|
||||
// stamp does not leave API-key bypass / hidden `/usage` stuck.
|
||||
if let Some(v) = update.subscription_tier_display {
|
||||
let is_key = super::super::app_view::is_api_key_label(&v);
|
||||
app.is_api_key_auth = is_key;
|
||||
app.usage_visible = !is_key && app.team_name.is_none();
|
||||
app.subscription_tier = Some(v);
|
||||
app.apply_tier_restrictions();
|
||||
}
|
||||
// TODO: extract resolve_session_picker_grouped helper (duplicates event_loop.rs:143-160)
|
||||
// Respect env var > config > remote precedence (mirrors event_loop.rs startup).
|
||||
if let Some(remote_val) = update.session_picker_grouped {
|
||||
@@ -142,33 +125,6 @@ pub(super) fn handle_settings_update(notif: &acp::ExtNotification, app: &mut App
|
||||
.unwrap_or(remote_val);
|
||||
app.session_picker_grouped = resolved;
|
||||
}
|
||||
if let Some(v) = update.subscription_watch_interval_secs {
|
||||
app.subscription_watch_interval_secs = Some(v);
|
||||
}
|
||||
|
||||
// Gate update logic:
|
||||
// - allow_access == Some(true): explicitly granted → lift the gate
|
||||
// - gate_message.is_some(): server sent a new message → impose/update
|
||||
// - Neither condition met: don't touch the gate. In particular,
|
||||
// allow_access=Some(false) without a gate_message must NOT clear the
|
||||
// gate (gate_from_settings returns None when gate_message is absent,
|
||||
// which would incorrectly lift an existing gate).
|
||||
if update.allow_access == Some(true) {
|
||||
let effs = app.lift_gate();
|
||||
app.pending_effects.extend(effs);
|
||||
} else if let Some(msg) = update.gate_message.as_ref()
|
||||
&& !msg.is_empty()
|
||||
{
|
||||
// (An empty gate_message would only clear the gate message text, NOT
|
||||
// access, so it intentionally does not touch the gate here.)
|
||||
let effs = app.impose_gate(kigi_shell::auth::GateInfo {
|
||||
message: msg.clone(),
|
||||
url: update.gate_url.clone(),
|
||||
label: update.gate_label.clone(),
|
||||
});
|
||||
app.pending_effects.extend(effs);
|
||||
}
|
||||
|
||||
// Load config layers once for tips + group_tool_verbs +
|
||||
// collapsed_edit_blocks resolution. Loaded unconditionally: the UI flags
|
||||
// re-resolve on every update (see below), and updates are rare (post-auth
|
||||
@@ -361,22 +317,10 @@ pub(super) struct PagerSettingsUpdate {
|
||||
#[serde(default)]
|
||||
show_resolved_model: Option<bool>,
|
||||
#[serde(default)]
|
||||
sharing_enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
session_picker_grouped: Option<bool>,
|
||||
#[serde(default)]
|
||||
tips: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
gate_message: Option<String>,
|
||||
#[serde(default)]
|
||||
gate_url: Option<String>,
|
||||
#[serde(default)]
|
||||
gate_label: Option<String>,
|
||||
#[serde(default)]
|
||||
allow_access: Option<bool>,
|
||||
#[serde(default)]
|
||||
subscription_tier_display: Option<String>,
|
||||
#[serde(default)]
|
||||
auto_permission_mode_enabled: Option<bool>,
|
||||
/// Soft-default permission mode. Presence-aware: omit = no update,
|
||||
/// `null` = recompute with remote=None, string = that soft-default.
|
||||
@@ -389,8 +333,6 @@ pub(super) struct PagerSettingsUpdate {
|
||||
group_tool_verbs: Option<bool>,
|
||||
#[serde(default)]
|
||||
collapsed_edit_blocks: Option<bool>,
|
||||
#[serde(default)]
|
||||
subscription_watch_interval_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// Presence-aware string: omit → `None` (`#[serde(default)]`), null →
|
||||
|
||||
@@ -33,8 +33,6 @@ pub(super) fn make_session(session_id: Option<&str>) -> AgentSession {
|
||||
restore_degree: None,
|
||||
rate_limited: false,
|
||||
model_incompatible: false,
|
||||
credit_limit_blocked: false,
|
||||
free_usage_blocked: false,
|
||||
available_commands: Vec::new(),
|
||||
available_commands_generation: 0,
|
||||
available_tools: None,
|
||||
@@ -257,17 +255,6 @@ pub(super) fn follow_ups_ext_with_prompt(
|
||||
std::sync::Arc::from(serde_json::value::to_raw_value(¶ms).unwrap()),
|
||||
)
|
||||
}
|
||||
pub(super) fn tier_settings_update(tier: &str) -> acp::ExtNotification {
|
||||
acp::ExtNotification::new(
|
||||
"x.ai/settings/update",
|
||||
std::sync::Arc::from(
|
||||
serde_json::value::to_raw_value(
|
||||
&serde_json::json!({ "subscription_tier_display" : tier }),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
pub(super) fn group_tool_verbs_settings_update(
|
||||
value: Option<bool>,
|
||||
) -> acp::ExtNotification {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_pending_tool(agent, "create-plan-call", "CreatePlan");
|
||||
agent.active_modal = Some(crate::views::modal::ActiveModal::CommandPalette {
|
||||
entries: crate::views::modal::default_palette_entries(agent.sharing_enabled),
|
||||
entries: crate::views::modal::default_palette_entries(),
|
||||
state: crate::views::picker::PickerState::input_active(),
|
||||
window: crate::views::modal_window::ModalWindowState::new(),
|
||||
});
|
||||
|
||||
@@ -1751,24 +1751,7 @@
|
||||
}
|
||||
|
||||
/// The credit-limit early return discards the popped adoption's buffer.
|
||||
#[test]
|
||||
fn credit_limit_response_discards_adoption_buffer() {
|
||||
let mut app = app_with_running_p1_and_stashed_b1();
|
||||
let id = AgentId(0);
|
||||
send_tool_call_update(&mut app, "b1", "bash-mode-1", None);
|
||||
app.agents
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.session
|
||||
.credit_limit_blocked = true;
|
||||
|
||||
prompt_response(&mut app, "p1");
|
||||
let agent = app.agents.get(&id).unwrap();
|
||||
assert!(!app.pending_running_adoptions.contains_key(&id));
|
||||
assert!(agent.pending_adoption_updates.is_empty());
|
||||
}
|
||||
|
||||
/// A stash whose pid replayed a durable terminal is discarded, never adopted.
|
||||
/// A stash whose pid replayed a durable terminal is discarded, never adopted.
|
||||
#[test]
|
||||
fn terminal_in_replay_stash_is_discarded_not_adopted() {
|
||||
let mut app = app_with_running_p1_and_stashed_b1();
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
apply_retry_state(&exhausted, &mut session, &mut scrollback, false);
|
||||
match last_session_event(&scrollback) {
|
||||
Some(SessionEvent::RetryFailed { error, .. }) => {
|
||||
assert_eq!(error, RATE_LIMITED_USER_MESSAGE_OAUTH);
|
||||
assert_eq!(error, RATE_LIMITED_USER_MESSAGE_OAUTH.as_str());
|
||||
}
|
||||
other => panic!("expected OAuth rate-limit RetryFailed, got {other:?}"),
|
||||
}
|
||||
@@ -194,140 +194,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
/// A rate-limit exhaustion whose flattened reason carries the
|
||||
/// free-usage code sets both flags and pushes NO generic block (the
|
||||
/// driver shows the paywall modal on PromptResponse; viewers keep no
|
||||
/// marker).
|
||||
#[test]
|
||||
fn retry_exhausted_free_usage_sets_paywall_flag_without_marker() {
|
||||
let mut session = make_session(Some("s1"));
|
||||
let mut scrollback = ScrollbackState::new();
|
||||
session.in_flight_prompt = Some(InFlightPrompt {
|
||||
text: "try me again".into(),
|
||||
images: Vec::new(),
|
||||
scrollback_entry: EntryId::new(2),
|
||||
chip_elements: Vec::new(),
|
||||
});
|
||||
|
||||
apply_retry_state(
|
||||
&RetryState::Exhausted {
|
||||
attempts: 0,
|
||||
reason: "API error (status 429 Too Many Requests): \
|
||||
subscription:free-usage-exhausted: You have used all your free usage."
|
||||
.into(),
|
||||
is_rate_limited: true,
|
||||
},
|
||||
&mut session,
|
||||
&mut scrollback,
|
||||
false,
|
||||
);
|
||||
assert!(
|
||||
session.rate_limited,
|
||||
"free-usage keeps rate_limited (TurnFailed/toast suppression)"
|
||||
);
|
||||
assert!(session.free_usage_blocked);
|
||||
assert_eq!(
|
||||
scrollback.len(),
|
||||
0,
|
||||
"no RetryFailed marker — the paywall modal shows instead"
|
||||
);
|
||||
assert!(
|
||||
session.in_flight_prompt.is_none(),
|
||||
"free-usage exhaustion clears in_flight_prompt like other failures"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_retry_state_credit_limit_exhausted_preserves_in_flight_prompt() {
|
||||
let mut session = make_session(Some("s1"));
|
||||
let mut scrollback = ScrollbackState::new();
|
||||
session.in_flight_prompt = Some(InFlightPrompt {
|
||||
text: "stash me".into(),
|
||||
images: Vec::new(),
|
||||
scrollback_entry: EntryId::new(2),
|
||||
chip_elements: Vec::new(),
|
||||
});
|
||||
apply_retry_state(
|
||||
&RetryState::Exhausted {
|
||||
attempts: 3,
|
||||
reason: "status 403: run out of credits".into(),
|
||||
is_rate_limited: false,
|
||||
},
|
||||
&mut session,
|
||||
&mut scrollback,
|
||||
false,
|
||||
);
|
||||
assert!(
|
||||
session.credit_limit_blocked,
|
||||
"credit_limit_blocked must be set for credit-limit 403"
|
||||
);
|
||||
assert!(
|
||||
session.in_flight_prompt.is_some(),
|
||||
"in_flight_prompt must be preserved so PromptResponse handler can stash it"
|
||||
);
|
||||
assert_eq!(session.in_flight_prompt.unwrap().text, "stash me");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_retry_state_credit_limit_failed_preserves_in_flight_prompt() {
|
||||
let mut session = make_session(Some("s1"));
|
||||
let mut scrollback = ScrollbackState::new();
|
||||
session.in_flight_prompt = Some(InFlightPrompt {
|
||||
text: "stash me too".into(),
|
||||
images: Vec::new(),
|
||||
scrollback_entry: EntryId::new(3),
|
||||
chip_elements: Vec::new(),
|
||||
});
|
||||
apply_retry_state(
|
||||
&RetryState::Failed {
|
||||
error_type: "proxy_error".into(),
|
||||
message: "status 403: run out of credits".into(),
|
||||
},
|
||||
&mut session,
|
||||
&mut scrollback,
|
||||
false,
|
||||
);
|
||||
assert!(
|
||||
session.credit_limit_blocked,
|
||||
"credit_limit_blocked must be set for credit-limit 403"
|
||||
);
|
||||
assert!(
|
||||
session.in_flight_prompt.is_some(),
|
||||
"in_flight_prompt must be preserved so PromptResponse handler can stash it"
|
||||
);
|
||||
assert_eq!(session.in_flight_prompt.unwrap().text, "stash me too");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_retry_state_pool_402_sets_credit_limit_blocked() {
|
||||
let mut session = make_session(Some("s1"));
|
||||
let mut scrollback = ScrollbackState::new();
|
||||
session.in_flight_prompt = Some(InFlightPrompt {
|
||||
text: "pool blocked".into(),
|
||||
images: Vec::new(),
|
||||
scrollback_entry: EntryId::new(5),
|
||||
chip_elements: Vec::new(),
|
||||
});
|
||||
apply_retry_state(
|
||||
&RetryState::Failed {
|
||||
error_type: "proxy_error".into(),
|
||||
message:
|
||||
"API error (status 402 Payment Required): Grok Build usage balance exhausted"
|
||||
.into(),
|
||||
},
|
||||
&mut session,
|
||||
&mut scrollback,
|
||||
false,
|
||||
);
|
||||
assert!(
|
||||
session.credit_limit_blocked,
|
||||
"credit_limit_blocked must be set for pool 402 balance exhausted"
|
||||
);
|
||||
assert!(session.in_flight_prompt.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_retry_state_non_credit_limit_failed_clears_in_flight_prompt() {
|
||||
fn apply_retry_state_generic_failed_clears_in_flight_prompt() {
|
||||
let mut session = make_session(Some("s1"));
|
||||
let mut scrollback = ScrollbackState::new();
|
||||
session.in_flight_prompt = Some(InFlightPrompt {
|
||||
@@ -345,13 +213,9 @@
|
||||
&mut scrollback,
|
||||
false,
|
||||
);
|
||||
assert!(
|
||||
!session.credit_limit_blocked,
|
||||
"credit_limit_blocked must NOT be set for non-credit-limit errors"
|
||||
);
|
||||
assert!(
|
||||
session.in_flight_prompt.is_none(),
|
||||
"in_flight_prompt must be cleared for non-credit-limit errors"
|
||||
"in_flight_prompt must be cleared for generic errors"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -400,7 +264,6 @@
|
||||
),
|
||||
"auth 401 must surface the actionable re-auth prompt"
|
||||
);
|
||||
assert!(!session.credit_limit_blocked);
|
||||
}
|
||||
|
||||
/// A recoverable auth failure preserves `in_flight_prompt` so the
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn settings_non_api_key_tier_clears_stale_api_key_flag() {
|
||||
let mut app = make_app_with_agent("sess-stale-key");
|
||||
assert!(handle_ext_notification(
|
||||
&tier_settings_update("API Key"),
|
||||
&mut app
|
||||
));
|
||||
assert!(app.is_api_key_auth);
|
||||
assert!(!app.usage_visible);
|
||||
assert!(app.tier_restricted_commands.is_empty());
|
||||
|
||||
// Later personal Free stamp must not keep the API-key bypass.
|
||||
assert!(handle_ext_notification(
|
||||
&tier_settings_update("Free"),
|
||||
&mut app
|
||||
));
|
||||
assert!(!app.is_api_key_auth);
|
||||
assert!(app.usage_visible);
|
||||
// Tier gating no longer exists; nothing gets re-restricted.
|
||||
assert!(app.tier_restricted_commands.is_empty());
|
||||
|
||||
// A paid tier after API Key clears the api-key flag and tier limits.
|
||||
let mut app = make_app_with_agent("sess-paid-tier");
|
||||
assert!(handle_ext_notification(
|
||||
&tier_settings_update("API Key"),
|
||||
&mut app
|
||||
));
|
||||
assert!(handle_ext_notification(
|
||||
&tier_settings_update("SuperGrok"),
|
||||
&mut app
|
||||
));
|
||||
assert!(!app.is_api_key_auth);
|
||||
assert!(app.tier_restricted_commands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_update_clearing_group_tool_verbs_reverts_to_default() {
|
||||
// Expected values come from the same chain the handler resolves, so the
|
||||
|
||||
Reference in new issue
Block a user