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"
158 lines
5.2 KiB
Rust
158 lines
5.2 KiB
Rust
use anyhow::Result;
|
|
use clap::Subcommand;
|
|
use kigi_shell::session::merge::MergedSession;
|
|
use kigi_shell::util::kigi_home::kigi_home;
|
|
#[derive(Debug, clap::Args, Clone)]
|
|
pub struct SessionsArgs {
|
|
#[command(subcommand)]
|
|
command: SessionsCommand,
|
|
}
|
|
|
|
#[derive(Debug, Subcommand, Clone)]
|
|
enum SessionsCommand {
|
|
/// List recent sessions (same as search with no query)
|
|
List {
|
|
/// Maximum number of sessions to show
|
|
#[arg(short = 'n', long, default_value = "20")]
|
|
limit: usize,
|
|
},
|
|
/// Search sessions by keyword
|
|
Search {
|
|
/// Search query (searches summaries and first prompts).
|
|
query: String,
|
|
/// Maximum number of sessions to show
|
|
#[arg(short = 'n', long, default_value = "20")]
|
|
limit: usize,
|
|
},
|
|
/// Permanently delete a session from history
|
|
Delete {
|
|
/// Session id to delete.
|
|
id: String,
|
|
},
|
|
}
|
|
|
|
pub async fn run(args: SessionsArgs) -> Result<()> {
|
|
let cwd = std::env::current_dir().unwrap_or_else(|_| ".".into());
|
|
|
|
match args.command {
|
|
SessionsCommand::List { limit } => {
|
|
let sessions =
|
|
kigi_shell::session::merge::fetch_merged(None, cwd.to_str(), None, limit).await;
|
|
print_sessions_grouped(&sessions);
|
|
}
|
|
SessionsCommand::Search { query, limit } => {
|
|
use kigi_shell::session::storage::search::{SessionSearchRequest, execute_search};
|
|
|
|
let req = SessionSearchRequest {
|
|
query,
|
|
cwd: Some(cwd.to_string_lossy().to_string()),
|
|
limit,
|
|
offset: 0,
|
|
include_content: true,
|
|
};
|
|
let root = kigi_home();
|
|
|
|
let resp = execute_search(&root, &req).await?;
|
|
|
|
for hit in &resp.results {
|
|
let title = if hit.title.is_empty() {
|
|
"(untitled)"
|
|
} else {
|
|
&hit.title
|
|
};
|
|
let time = chrono::DateTime::from_timestamp(hit.updated_at_unix, 0)
|
|
.map(|dt| {
|
|
dt.with_timezone(&chrono::Local)
|
|
.format("%b %d, %l:%M%P")
|
|
.to_string()
|
|
})
|
|
.unwrap_or_default();
|
|
println!(
|
|
"{} (score: {:.2}) {}\n {}\n {}",
|
|
hit.session_id,
|
|
hit.score,
|
|
time,
|
|
title,
|
|
hit.snippet.as_deref().unwrap_or("")
|
|
);
|
|
}
|
|
|
|
println!("\nTotal: {}", resp.results.len());
|
|
}
|
|
SessionsCommand::Delete { id } => {
|
|
// Pass `cwd = None` so the session is found by id regardless of
|
|
// which workspace it was created in; the local delete still uses
|
|
// the resolved per-session cwd.
|
|
let deletion =
|
|
kigi_shell::session::persistence::delete_session_history(&id, None).await?;
|
|
|
|
if deletion.any_removed() {
|
|
println!("Deleted session {id}");
|
|
} else {
|
|
println!("No session found with id {id}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Print sessions grouped by worktree label, preserving the original table
|
|
/// format with a `Label: <label>` header before each group.
|
|
fn print_sessions_grouped(sessions: &[MergedSession]) {
|
|
if sessions.is_empty() {
|
|
println!("No sessions found.");
|
|
return;
|
|
}
|
|
|
|
// Group by worktree_label, sort alphabetically, None last.
|
|
let mut groups: std::collections::BTreeMap<Option<&str>, Vec<&MergedSession>> =
|
|
std::collections::BTreeMap::new();
|
|
for s in sessions {
|
|
groups
|
|
.entry(s.worktree_label.as_deref())
|
|
.or_default()
|
|
.push(s);
|
|
}
|
|
|
|
let header = format!(
|
|
"{:<36} {:<10} {:<10} {:<10} {}",
|
|
"SESSION ID", "CREATED", "UPDATED", "STATUS", "SUMMARY"
|
|
);
|
|
|
|
// Labeled groups first (alphabetical), then unlabeled last.
|
|
let none_group = groups.remove(&None);
|
|
let print_group = |label_line: &str, members: &[&MergedSession]| {
|
|
println!("\n{label_line}");
|
|
println!("{header}");
|
|
for s in members {
|
|
let first_line;
|
|
let summary: &str = if !s.summary.is_empty() {
|
|
&s.summary
|
|
} else if let Some(ref fp) = s.first_prompt
|
|
&& let Some(line) = fp.lines().find(|l| !l.trim().is_empty())
|
|
{
|
|
first_line = line.trim().to_string();
|
|
&first_line
|
|
} else {
|
|
"(no summary)"
|
|
};
|
|
let truncated: String = summary.chars().take(50).collect();
|
|
let created = &s.created_at[..s.created_at.len().min(10)];
|
|
let updated = &s.updated_at[..s.updated_at.len().min(10)];
|
|
println!(
|
|
"{} {} {} {} {}",
|
|
s.session_id, created, updated, s.source, truncated
|
|
);
|
|
}
|
|
};
|
|
|
|
for (label, members) in &groups {
|
|
let line = format!("Label: {}", label.unwrap_or(""));
|
|
print_group(&line, members);
|
|
}
|
|
if let Some(members) = &none_group {
|
|
print_group("(no label)", members);
|
|
}
|
|
}
|