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:
@@ -13,11 +13,10 @@ use crate::prompt_images::{InlineMediaInfo, ScrollbackImageRef, ScrollbackVideoR
|
||||
|
||||
use super::blocks::mermaid_content::DiagramAffordance;
|
||||
use super::blocks::{
|
||||
AgentMessageBlock, BgTaskBlock, BtwBlock, ContextInfoBlock, CreditLimitBlock,
|
||||
EditToolCallBlock, ExecuteToolCallBlock, LineRange, ListDirToolCallBlock, OtherToolCallBlock,
|
||||
ReadToolCallBlock, SearchFileMatch, SearchToolCallBlock, SessionEvent, SessionEventBlock,
|
||||
SubagentBlock, SubagentBlockKind, SystemMessageBlock, ThinkingBlock, ToolCallBlock,
|
||||
UserPromptBlock,
|
||||
AgentMessageBlock, BgTaskBlock, BtwBlock, ContextInfoBlock, EditToolCallBlock,
|
||||
ExecuteToolCallBlock, LineRange, ListDirToolCallBlock, OtherToolCallBlock, ReadToolCallBlock,
|
||||
SearchFileMatch, SearchToolCallBlock, SessionEvent, SessionEventBlock, SubagentBlock,
|
||||
SubagentBlockKind, SystemMessageBlock, ThinkingBlock, ToolCallBlock, UserPromptBlock,
|
||||
};
|
||||
use super::types::{
|
||||
AccentStyle, BlockBackground, BlockContext, BlockOutput, DisplayMode, RenderedBlockOutput,
|
||||
@@ -383,8 +382,6 @@ pub enum RenderBlock {
|
||||
Btw(BtwBlock),
|
||||
/// `/context` snapshot with categorical bar + breakdown.
|
||||
ContextInfo(ContextInfoBlock),
|
||||
/// Credit-limit card for max-tier users (red accent, single action).
|
||||
CreditLimit(CreditLimitBlock),
|
||||
}
|
||||
|
||||
/// Delegate a method call to the inner block variant.
|
||||
@@ -402,7 +399,6 @@ macro_rules! delegate_block {
|
||||
RenderBlock::Subagent(b) => b.$method($($arg),*),
|
||||
RenderBlock::Btw(b) => b.$method($($arg),*),
|
||||
RenderBlock::ContextInfo(b) => b.$method($($arg),*),
|
||||
RenderBlock::CreditLimit(b) => b.$method($($arg),*),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -778,15 +774,6 @@ impl RenderBlock {
|
||||
RenderBlock::SessionEvent(SessionEventBlock::new(event))
|
||||
}
|
||||
|
||||
/// Create a credit-limit card (inline scrollback block for max-tier users).
|
||||
pub fn credit_limit_card(
|
||||
heading: impl Into<String>,
|
||||
action: crate::scrollback::blocks::CreditLimitCardAction,
|
||||
url: impl Into<String>,
|
||||
) -> Self {
|
||||
RenderBlock::CreditLimit(CreditLimitBlock::new(heading, action, url))
|
||||
}
|
||||
|
||||
/// Create a "Task started" background task block.
|
||||
pub fn bg_task(command: impl Into<String>, task_id: impl Into<String>) -> Self {
|
||||
RenderBlock::BgTask(BgTaskBlock::started(command, task_id))
|
||||
@@ -912,11 +899,6 @@ impl RenderBlock {
|
||||
matches!(self, RenderBlock::AgentMessage(_))
|
||||
}
|
||||
|
||||
/// Check if this block is a CreditLimit card.
|
||||
pub fn is_credit_limit(&self) -> bool {
|
||||
matches!(self, RenderBlock::CreditLimit(_))
|
||||
}
|
||||
|
||||
/// Check if this block is a plan mode tool call (enter or exit).
|
||||
///
|
||||
/// Exact-matches the canonical tool-name set rather than substring-matching
|
||||
@@ -1014,10 +996,9 @@ impl RenderBlock {
|
||||
None
|
||||
}
|
||||
}
|
||||
RenderBlock::System(_)
|
||||
| RenderBlock::SessionEvent(_)
|
||||
| RenderBlock::ContextInfo(_)
|
||||
| RenderBlock::CreditLimit(_) => None,
|
||||
RenderBlock::System(_) | RenderBlock::SessionEvent(_) | RenderBlock::ContextInfo(_) => {
|
||||
None
|
||||
}
|
||||
RenderBlock::Btw(_) => Some(theme.accent_plan),
|
||||
RenderBlock::Stub(block) => Some(block.accent_color),
|
||||
}
|
||||
@@ -1161,9 +1142,6 @@ impl RenderBlock {
|
||||
Some(b.content().rendered_plain_text()),
|
||||
]),
|
||||
RenderBlock::ContextInfo(b) => join_searchable([Some(b.model.clone())]),
|
||||
RenderBlock::CreditLimit(b) => {
|
||||
join_searchable([Some(b.heading.clone()), Some(b.url.clone())])
|
||||
}
|
||||
RenderBlock::ToolCall(tc) => tc.searchable_text(),
|
||||
}
|
||||
}
|
||||
@@ -1566,18 +1544,6 @@ mod searchable_text_tests {
|
||||
assert_eq!(block.searchable_text().as_deref(), Some("grok-4.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credit_limit_indexes_heading_and_url() {
|
||||
let block = RenderBlock::credit_limit_card(
|
||||
"credit limit reached",
|
||||
crate::scrollback::blocks::CreditLimitCardAction::EnablePayg,
|
||||
"https://grok.com?_s=usage",
|
||||
);
|
||||
let text = block.searchable_text().expect("credit limit text");
|
||||
assert!(text.contains("credit limit reached"), "got: {text:?}");
|
||||
assert!(text.contains("https://grok.com?_s=usage"), "got: {text:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_indexes_pattern_and_match_line() {
|
||||
let block = RenderBlock::search(
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
//! CreditLimitBlock — scrollback card shown when a max-tier user exhausts credits.
|
||||
//!
|
||||
//! Replaces the Q&A question modal for users already at the highest tier
|
||||
//! (SuperGrok Heavy). Instead of offering "Upgrade tier" + PAYG / buy-credits
|
||||
//! options in the question overlay, this block renders an inline card with a
|
||||
//! descriptive message and a link to the usage/billing page.
|
||||
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
use crate::scrollback::block::BlockContent;
|
||||
use crate::scrollback::types::{AccentStyle, BlockContext, BlockLine, BlockOutput, DisplayMode};
|
||||
use crate::theme::Theme;
|
||||
|
||||
/// Which continue-path the max-tier credit-limit card recommends.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CreditLimitCardAction {
|
||||
/// Legacy on-demand: PAYG not enabled yet.
|
||||
EnablePayg,
|
||||
/// Legacy on-demand: PAYG on but at spending cap.
|
||||
IncreasePaygLimit,
|
||||
/// Unified usage billing: purchase prepaid credits.
|
||||
PurchaseCredits,
|
||||
}
|
||||
|
||||
/// Inline scrollback card for credit-limit exhaustion on max-tier accounts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreditLimitBlock {
|
||||
/// Card heading (e.g. "You've hit your free credits limit.").
|
||||
pub heading: String,
|
||||
/// Continue-path body copy selector.
|
||||
pub action: CreditLimitCardAction,
|
||||
/// URL to the usage/billing page.
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl CreditLimitBlock {
|
||||
/// Create a new credit-limit card.
|
||||
pub fn new(
|
||||
heading: impl Into<String>,
|
||||
action: CreditLimitCardAction,
|
||||
url: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
heading: heading.into(),
|
||||
action,
|
||||
url: url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockContent for CreditLimitBlock {
|
||||
fn output(&self, _ctx: &BlockContext) -> BlockOutput {
|
||||
let theme = Theme::current();
|
||||
|
||||
// Heading in bold warning color (amber/yellow).
|
||||
let heading_style = Style::default()
|
||||
.fg(theme.warning)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let heading = Line::from(Span::styled(self.heading.clone(), heading_style));
|
||||
|
||||
// Body copy — contextual message based on billing mode.
|
||||
let muted = theme.muted();
|
||||
let body = match self.action {
|
||||
CreditLimitCardAction::IncreasePaygLimit => {
|
||||
"You can continue by increasing your spending limit."
|
||||
}
|
||||
CreditLimitCardAction::EnablePayg => {
|
||||
"You can continue by enabling pay-as-you-go usage."
|
||||
}
|
||||
CreditLimitCardAction::PurchaseCredits => {
|
||||
"You can continue by purchasing more credits."
|
||||
}
|
||||
};
|
||||
let body_line = Line::from(Span::styled(body.to_string(), muted));
|
||||
|
||||
// Clickable link styled as a button.
|
||||
let link_style = theme.link_style();
|
||||
let link_line = Line::from(vec![Span::styled(self.url.clone(), link_style)]);
|
||||
|
||||
BlockOutput {
|
||||
lines: vec![
|
||||
BlockLine::styled(heading).with_selection_range(Some(0)),
|
||||
BlockLine::separator(Line::from("")),
|
||||
BlockLine::styled(body_line).with_selection_range(Some(0)),
|
||||
BlockLine::styled(link_line).with_selection_range(Some(0)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn accent(&self, _ctx: &BlockContext) -> Option<AccentStyle> {
|
||||
let theme = Theme::current();
|
||||
Some(AccentStyle::static_color(theme.warning))
|
||||
}
|
||||
|
||||
fn has_vpad(&self, _ctx: &BlockContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn has_raw_mode(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_foldable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_display_mode(&self) -> DisplayMode {
|
||||
DisplayMode::Expanded
|
||||
}
|
||||
|
||||
fn is_selectable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_groupable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::appearance::AppearanceConfig;
|
||||
|
||||
fn ctx() -> BlockContext {
|
||||
BlockContext {
|
||||
mode: DisplayMode::Expanded,
|
||||
is_running: false,
|
||||
width: 80,
|
||||
raw: false,
|
||||
max_lines: None,
|
||||
appearance: AppearanceConfig::default(),
|
||||
is_selected: false,
|
||||
cwd: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_payg_off_mentions_enabling() {
|
||||
let block = CreditLimitBlock::new(
|
||||
"You\u{2019}ve hit your credit limit.",
|
||||
CreditLimitCardAction::EnablePayg,
|
||||
"https://grok.com?_s=usage",
|
||||
);
|
||||
let output = block.output(&ctx());
|
||||
let all_text: String = output
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|l| l.content.spans.iter().map(|s| s.content.as_ref()))
|
||||
.collect();
|
||||
assert!(all_text.contains("credit limit"));
|
||||
assert!(all_text.contains("enabling pay-as-you-go"));
|
||||
assert!(all_text.contains("grok.com?_s=usage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_payg_on_mentions_increasing() {
|
||||
let block = CreditLimitBlock::new(
|
||||
"You\u{2019}ve hit your spending cap.",
|
||||
CreditLimitCardAction::IncreasePaygLimit,
|
||||
"https://grok.com?_s=usage",
|
||||
);
|
||||
let output = block.output(&ctx());
|
||||
let all_text: String = output
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|l| l.content.spans.iter().map(|s| s.content.as_ref()))
|
||||
.collect();
|
||||
assert!(all_text.contains("spending cap"));
|
||||
assert!(all_text.contains("increasing your spending limit"));
|
||||
assert!(all_text.contains("grok.com?_s=usage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_unified_mentions_purchasing_credits() {
|
||||
let block = CreditLimitBlock::new(
|
||||
"You hit your weekly limit.",
|
||||
CreditLimitCardAction::PurchaseCredits,
|
||||
"https://grok.com?_s=usage",
|
||||
);
|
||||
let output = block.output(&ctx());
|
||||
let all_text: String = output
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|l| l.content.spans.iter().map(|s| s.content.as_ref()))
|
||||
.collect();
|
||||
assert!(all_text.contains("purchasing more credits"));
|
||||
assert!(all_text.contains("grok.com?_s=usage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_warning_accent() {
|
||||
let block = CreditLimitBlock::new("heading", CreditLimitCardAction::EnablePayg, "url");
|
||||
let accent = block.accent(&ctx());
|
||||
let theme = Theme::current();
|
||||
assert!(accent.is_some());
|
||||
assert_eq!(accent.unwrap().color, theme.warning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_content_contract() {
|
||||
let block = CreditLimitBlock::new("heading", CreditLimitCardAction::EnablePayg, "url");
|
||||
let c = ctx();
|
||||
assert!(!block.is_foldable());
|
||||
assert!(block.is_selectable());
|
||||
assert!(!block.is_groupable());
|
||||
assert!(matches!(
|
||||
block.default_display_mode(),
|
||||
DisplayMode::Expanded
|
||||
));
|
||||
assert!(block.has_vpad(&c));
|
||||
assert!(!block.has_raw_mode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_structure_and_content() {
|
||||
let url = "https://grok.com?_s=usage";
|
||||
let block = CreditLimitBlock::new("Test heading", CreditLimitCardAction::EnablePayg, url);
|
||||
let output = block.output(&ctx());
|
||||
|
||||
// heading, separator, body, link = 4 lines
|
||||
assert_eq!(output.lines.len(), 4);
|
||||
|
||||
let all_text: String = output
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|l| l.content.spans.iter().map(|s| s.content.as_ref()))
|
||||
.collect();
|
||||
assert!(all_text.contains(url));
|
||||
|
||||
// Heading uses bold modifier.
|
||||
assert!(
|
||||
output.lines[0]
|
||||
.content
|
||||
.spans
|
||||
.iter()
|
||||
.any(|s| s.style.add_modifier.contains(Modifier::BOLD))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_stores_fields_correctly() {
|
||||
let block = CreditLimitBlock::new(
|
||||
"my heading",
|
||||
CreditLimitCardAction::IncreasePaygLimit,
|
||||
"https://example.com",
|
||||
);
|
||||
assert_eq!(block.heading, "my heading");
|
||||
assert_eq!(block.action, CreditLimitCardAction::IncreasePaygLimit);
|
||||
assert_eq!(block.url, "https://example.com");
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ mod agent;
|
||||
mod bg_task;
|
||||
mod btw;
|
||||
mod context_info;
|
||||
mod credit_limit;
|
||||
pub mod markdown_content;
|
||||
pub mod mermaid_content;
|
||||
mod quote_bar;
|
||||
@@ -21,7 +20,6 @@ pub use agent::AgentMessageBlock;
|
||||
pub use bg_task::{BgTaskBlock, BgTaskKind};
|
||||
pub use btw::BtwBlock;
|
||||
pub use context_info::ContextInfoBlock;
|
||||
pub use credit_limit::{CreditLimitBlock, CreditLimitCardAction};
|
||||
pub use session_event::{EndWork, SessionEvent, SessionEventBlock};
|
||||
pub use subagent::{SubagentBlock, SubagentBlockKind};
|
||||
pub use system::SystemMessageBlock;
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn render_blocks_to_markdown<'a>(blocks: impl IntoIterator<Item = &'a Render
|
||||
last_was_agent = false;
|
||||
}
|
||||
// Skip all non-conversation chrome: Thinking, System, SessionEvent, BgTask,
|
||||
// Subagent, Btw, CreditLimit, Stub, etc. Thinking blocks are
|
||||
// Subagent, Btw, Stub, etc. Thinking blocks are
|
||||
// treated as intra-Assistant glue (no new header).
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ impl<'a> EntryRenderer<'a> {
|
||||
///
|
||||
/// EXACT for blocks whose searchable text mirrors their selectable rendered
|
||||
/// lines (plain source blocks, markdown/thinking bodies); a best-effort
|
||||
/// estimate for field-joined source (Subagent/BgTask/CreditLimit), kept on
|
||||
/// estimate for field-joined source (Subagent/BgTask/etc.), kept on
|
||||
/// screen by the caller's entry-height clamp. Past the last logical line,
|
||||
/// clamps to the final content row.
|
||||
pub fn rendered_row_of_logical_line(&self, width: u16, logical_line: usize) -> u16 {
|
||||
|
||||
Reference in New Issue
Block a user