Skip to content

Commit f885561

Browse files
MarkShawn2020claude
andcommitted
fix(chat): 修复 session summary 中 slash command 格式转换问题
- 简化 Rust regex 策略,分步提取 command name 和 args - 调整处理顺序:先格式转换再截断,避免破坏 XML 结构 - 添加 get_session_summary 命令,MessageView 主动获取最新 summary - 提取 SessionMenuItems 共享组件(DRY) - 添加 lazy_static 依赖支持 regex 缓存 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bc70d74 commit f885561

9 files changed

Lines changed: 691 additions & 102 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ serde = { version = "1", features = ["derive"] }
2525
serde_json = "1"
2626
dirs = "6"
2727
regex = "1"
28+
lazy_static = "1"
2829
tantivy = "0.22"
2930
jieba-rs = "0.7"
3031
notify = "7"

src-tauri/src/lib.rs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ fn read_session_head(path: &Path, max_lines: usize) -> (Option<String>, usize) {
498498

499499
let reader = BufReader::new(file);
500500
let mut summary = None;
501+
let mut first_user_message: Option<String> = None;
501502
let mut message_count = 0;
502503

503504
for line in reader.lines().take(max_lines) {
@@ -509,15 +510,90 @@ fn read_session_head(path: &Path, max_lines: usize) -> (Option<String>, usize) {
509510
if parsed.line_type.as_deref() == Some("summary") {
510511
summary = parsed.summary;
511512
}
512-
if parsed.line_type.as_deref() == Some("user")
513-
|| parsed.line_type.as_deref() == Some("assistant")
514-
{
513+
if parsed.line_type.as_deref() == Some("user") {
514+
message_count += 1;
515+
// Capture first user message as fallback summary
516+
if first_user_message.is_none() {
517+
if let Some(msg) = &parsed.message {
518+
if let Some(content) = &msg.content {
519+
// Extract text from content (can be string or array)
520+
let text_content = match content {
521+
serde_json::Value::String(s) => Some(s.clone()),
522+
serde_json::Value::Array(arr) => {
523+
// Find first text block
524+
arr.iter().find_map(|item| {
525+
if item.get("type").and_then(|t| t.as_str()) == Some("text") {
526+
item.get("text").and_then(|t| t.as_str()).map(|s| s.to_string())
527+
} else {
528+
None
529+
}
530+
})
531+
}
532+
_ => None,
533+
};
534+
if let Some(text) = text_content {
535+
// Apply restore_slash_command BEFORE truncation to preserve XML structure
536+
let restored = restore_slash_command(&text);
537+
// Truncate to reasonable length for display (UTF-8 safe)
538+
let display = if restored.chars().count() > 80 {
539+
format!("{}...", restored.chars().take(80).collect::<String>())
540+
} else {
541+
restored
542+
};
543+
first_user_message = Some(display);
544+
}
545+
}
546+
}
547+
}
548+
}
549+
if parsed.line_type.as_deref() == Some("assistant") {
515550
message_count += 1;
516551
}
517552
}
518553
}
519554

520-
(summary, message_count)
555+
// Use summary if available, otherwise fall back to first user message
556+
let final_summary = summary.or(first_user_message).map(|s| restore_slash_command(&s));
557+
(final_summary, message_count)
558+
}
559+
560+
/// Convert <command-message>...</command-message><command-name>/cmd</command-name> to /cmd format
561+
fn restore_slash_command(content: &str) -> String {
562+
use regex::Regex;
563+
lazy_static::lazy_static! {
564+
// Extract command name
565+
static ref NAME_RE: Regex = Regex::new(r"<command-name>(/[^<]+)</command-name>").unwrap();
566+
// Extract args (handles multi-line)
567+
static ref ARGS_RE: Regex = Regex::new(r"(?s)<command-args>(.*?)</command-args>").unwrap();
568+
// Strip all command-related XML tags
569+
static ref STRIP_RE: Regex = Regex::new(r"(?s)<command-message>.*?</command-message>|</?command-[^>]*>").unwrap();
570+
}
571+
572+
// Extract command name and args first
573+
let cmd = NAME_RE.captures(content)
574+
.and_then(|c| c.get(1))
575+
.map(|m| m.as_str().to_string());
576+
let args = ARGS_RE.captures(content)
577+
.and_then(|c| c.get(1))
578+
.map(|m| m.as_str().trim().to_string())
579+
.filter(|s| !s.is_empty());
580+
581+
// Build result: command + args, then strip remaining tags
582+
let prefix = match (cmd, args) {
583+
(Some(c), Some(a)) => format!("{} {}", c, a),
584+
(Some(c), None) => c,
585+
_ => String::new(),
586+
};
587+
588+
// Strip all command tags from original content
589+
let cleaned = STRIP_RE.replace_all(content, "").trim().to_string();
590+
591+
// If we extracted a command, return it; otherwise return cleaned content
592+
if prefix.is_empty() {
593+
cleaned
594+
} else {
595+
prefix
596+
}
521597
}
522598

523599
/// Build session index from history.jsonl (fast: only reads one file)
@@ -591,8 +667,8 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
591667
// Only read head for summary (first 20 lines should be enough)
592668
let (summary, head_msg_count) = read_session_head(&session_path, 20);
593669

594-
// Use display as fallback summary
595-
let final_summary = summary.or_else(|| display.clone());
670+
// Use display as fallback summary (also needs restore_slash_command)
671+
let final_summary = summary.or_else(|| display.clone().map(|d| restore_slash_command(&d)));
596672

597673
// Use file mtime for accurate last_modified
598674
let metadata = fs::metadata(&session_path).ok();
@@ -4291,6 +4367,16 @@ fn get_session_file_path(project_id: String, session_id: String) -> Result<Strin
42914367
Ok(path.to_string_lossy().to_string())
42924368
}
42934369

4370+
#[tauri::command]
4371+
fn get_session_summary(project_id: String, session_id: String) -> Result<Option<String>, String> {
4372+
let path = get_session_path(&project_id, &session_id);
4373+
if !path.exists() {
4374+
return Err("Session file not found".to_string());
4375+
}
4376+
let (summary, _) = read_session_head(&path, 20);
4377+
Ok(summary)
4378+
}
4379+
42944380
#[tauri::command]
42954381
fn copy_to_clipboard(text: String) -> Result<(), String> {
42964382
let mut clipboard = arboard::Clipboard::new().map_err(|e| e.to_string())?;
@@ -6171,6 +6257,7 @@ pub fn run() {
61716257
reveal_path,
61726258
open_path,
61736259
get_session_file_path,
6260+
get_session_summary,
61746261
copy_to_clipboard,
61756262
get_settings_path,
61766263
get_mcp_config_path,

0 commit comments

Comments
 (0)