mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
feat: b5-doctor-cmd — batch 5 wave 2
This commit is contained in:
@@ -504,6 +504,10 @@ where
|
||||
&self.session
|
||||
}
|
||||
|
||||
pub fn session_mut(&mut self) -> &mut Session {
|
||||
&mut self.session
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fork_session(&self, branch_name: Option<String>) -> Session {
|
||||
self.session.fork(branch_name)
|
||||
|
||||
@@ -145,7 +145,7 @@ pub use sandbox::{
|
||||
};
|
||||
pub use session::{
|
||||
ContentBlock, ConversationMessage, MessageRole, Session, SessionCompaction, SessionError,
|
||||
SessionFork,
|
||||
SessionFork, SessionPromptEntry,
|
||||
};
|
||||
pub use sse::{IncrementalSseParser, SseEvent};
|
||||
pub use stale_branch::{
|
||||
|
||||
@@ -65,6 +65,13 @@ pub struct SessionFork {
|
||||
pub branch_name: Option<String>,
|
||||
}
|
||||
|
||||
/// A single user prompt recorded with a timestamp for history tracking.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SessionPromptEntry {
|
||||
pub timestamp_ms: u64,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct SessionPersistence {
|
||||
path: PathBuf,
|
||||
@@ -88,6 +95,7 @@ pub struct Session {
|
||||
pub compaction: Option<SessionCompaction>,
|
||||
pub fork: Option<SessionFork>,
|
||||
pub workspace_root: Option<PathBuf>,
|
||||
pub prompt_history: Vec<SessionPromptEntry>,
|
||||
persistence: Option<SessionPersistence>,
|
||||
}
|
||||
|
||||
@@ -101,6 +109,7 @@ impl PartialEq for Session {
|
||||
&& self.compaction == other.compaction
|
||||
&& self.fork == other.fork
|
||||
&& self.workspace_root == other.workspace_root
|
||||
&& self.prompt_history == other.prompt_history
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +160,7 @@ impl Session {
|
||||
compaction: None,
|
||||
fork: None,
|
||||
workspace_root: None,
|
||||
prompt_history: Vec::new(),
|
||||
persistence: None,
|
||||
}
|
||||
}
|
||||
@@ -252,6 +262,7 @@ impl Session {
|
||||
branch_name: normalize_optional_string(branch_name),
|
||||
}),
|
||||
workspace_root: self.workspace_root.clone(),
|
||||
prompt_history: self.prompt_history.clone(),
|
||||
persistence: None,
|
||||
}
|
||||
}
|
||||
@@ -295,6 +306,17 @@ impl Session {
|
||||
JsonValue::String(workspace_root_to_string(workspace_root)?),
|
||||
);
|
||||
}
|
||||
if !self.prompt_history.is_empty() {
|
||||
object.insert(
|
||||
"prompt_history".to_string(),
|
||||
JsonValue::Array(
|
||||
self.prompt_history
|
||||
.iter()
|
||||
.map(SessionPromptEntry::to_jsonl_record)
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
Ok(JsonValue::Object(object))
|
||||
}
|
||||
|
||||
@@ -339,6 +361,16 @@ impl Session {
|
||||
.get("workspace_root")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(PathBuf::from);
|
||||
let prompt_history = object
|
||||
.get("prompt_history")
|
||||
.and_then(JsonValue::as_array)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(SessionPromptEntry::from_json_opt)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(Self {
|
||||
version,
|
||||
session_id,
|
||||
@@ -348,6 +380,7 @@ impl Session {
|
||||
compaction,
|
||||
fork,
|
||||
workspace_root,
|
||||
prompt_history,
|
||||
persistence: None,
|
||||
})
|
||||
}
|
||||
@@ -361,6 +394,7 @@ impl Session {
|
||||
let mut compaction = None;
|
||||
let mut fork = None;
|
||||
let mut workspace_root = None;
|
||||
let mut prompt_history = Vec::new();
|
||||
|
||||
for (line_number, raw_line) in contents.lines().enumerate() {
|
||||
let line = raw_line.trim();
|
||||
@@ -414,6 +448,13 @@ impl Session {
|
||||
object.clone(),
|
||||
))?);
|
||||
}
|
||||
"prompt_history" => {
|
||||
if let Some(entry) =
|
||||
SessionPromptEntry::from_json_opt(&JsonValue::Object(object.clone()))
|
||||
{
|
||||
prompt_history.push(entry);
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(SessionError::Format(format!(
|
||||
"unsupported JSONL record type at line {}: {other}",
|
||||
@@ -433,15 +474,36 @@ impl Session {
|
||||
compaction,
|
||||
fork,
|
||||
workspace_root,
|
||||
prompt_history,
|
||||
persistence: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Record a user prompt with the current wall-clock timestamp.
|
||||
///
|
||||
/// The entry is appended to the in-memory history and, when a persistence
|
||||
/// path is configured, incrementally written to the JSONL session file.
|
||||
pub fn push_prompt_entry(&mut self, text: impl Into<String>) -> Result<(), SessionError> {
|
||||
let timestamp_ms = current_time_millis();
|
||||
let entry = SessionPromptEntry {
|
||||
timestamp_ms,
|
||||
text: text.into(),
|
||||
};
|
||||
self.prompt_history.push(entry);
|
||||
let entry_ref = self.prompt_history.last().expect("entry was just pushed");
|
||||
self.append_persisted_prompt_entry(entry_ref)
|
||||
}
|
||||
|
||||
fn render_jsonl_snapshot(&self) -> Result<String, SessionError> {
|
||||
let mut lines = vec![self.meta_record()?.render()];
|
||||
if let Some(compaction) = &self.compaction {
|
||||
lines.push(compaction.to_jsonl_record()?.render());
|
||||
}
|
||||
lines.extend(
|
||||
self.prompt_history
|
||||
.iter()
|
||||
.map(|entry| entry.to_jsonl_record().render()),
|
||||
);
|
||||
lines.extend(
|
||||
self.messages
|
||||
.iter()
|
||||
@@ -468,6 +530,25 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_persisted_prompt_entry(
|
||||
&self,
|
||||
entry: &SessionPromptEntry,
|
||||
) -> Result<(), SessionError> {
|
||||
let Some(path) = self.persistence_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let needs_bootstrap = !path.exists() || fs::metadata(path)?.len() == 0;
|
||||
if needs_bootstrap {
|
||||
self.save_to_path(path)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut file = OpenOptions::new().append(true).open(path)?;
|
||||
writeln!(file, "{}", entry.to_jsonl_record().render())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn meta_record(&self) -> Result<JsonValue, SessionError> {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert(
|
||||
@@ -784,6 +865,33 @@ impl SessionFork {
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionPromptEntry {
|
||||
#[must_use]
|
||||
pub fn to_jsonl_record(&self) -> JsonValue {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert(
|
||||
"type".to_string(),
|
||||
JsonValue::String("prompt_history".to_string()),
|
||||
);
|
||||
object.insert(
|
||||
"timestamp_ms".to_string(),
|
||||
JsonValue::Number(i64::try_from(self.timestamp_ms).unwrap_or(i64::MAX)),
|
||||
);
|
||||
object.insert("text".to_string(), JsonValue::String(self.text.clone()));
|
||||
JsonValue::Object(object)
|
||||
}
|
||||
|
||||
fn from_json_opt(value: &JsonValue) -> Option<Self> {
|
||||
let object = value.as_object()?;
|
||||
let timestamp_ms = object
|
||||
.get("timestamp_ms")
|
||||
.and_then(JsonValue::as_i64)
|
||||
.and_then(|value| u64::try_from(value).ok())?;
|
||||
let text = object.get("text").and_then(JsonValue::as_str)?.to_string();
|
||||
Some(Self { timestamp_ms, text })
|
||||
}
|
||||
}
|
||||
|
||||
fn message_record(message: &ConversationMessage) -> JsonValue {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert("type".to_string(), JsonValue::String("message".to_string()));
|
||||
|
||||
@@ -3642,10 +3642,14 @@ impl LiveCli {
|
||||
.map_or(self.runtime.session().updated_at_ms, |duration| {
|
||||
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
|
||||
});
|
||||
self.prompt_history.push(PromptHistoryEntry {
|
||||
let entry = PromptHistoryEntry {
|
||||
timestamp_ms,
|
||||
text: prompt.to_string(),
|
||||
});
|
||||
};
|
||||
self.prompt_history.push(entry);
|
||||
if let Err(error) = self.runtime.session_mut().push_prompt_entry(prompt) {
|
||||
eprintln!("warning: failed to persist prompt history: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn print_prompt_history(&self, count: Option<&str>) {
|
||||
@@ -3656,10 +3660,27 @@ impl LiveCli {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let entries = if self.prompt_history.is_empty() {
|
||||
collect_session_prompt_history(self.runtime.session())
|
||||
let session_entries = &self.runtime.session().prompt_history;
|
||||
let entries = if session_entries.is_empty() {
|
||||
if self.prompt_history.is_empty() {
|
||||
collect_session_prompt_history(self.runtime.session())
|
||||
} else {
|
||||
self.prompt_history
|
||||
.iter()
|
||||
.map(|entry| PromptHistoryEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
text: entry.text.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
} else {
|
||||
self.prompt_history.clone()
|
||||
session_entries
|
||||
.iter()
|
||||
.map(|entry| PromptHistoryEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
text: entry.text.clone(),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
println!("{}", render_prompt_history_report(&entries, limit));
|
||||
}
|
||||
@@ -5145,7 +5166,7 @@ fn write_temp_text_file(
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
const DEFAULT_HISTORY_LIMIT: usize = 10;
|
||||
const DEFAULT_HISTORY_LIMIT: usize = 20;
|
||||
|
||||
fn parse_history_count(raw: Option<&str>) -> Result<usize, String> {
|
||||
let Some(raw) = raw else {
|
||||
@@ -5222,6 +5243,16 @@ fn render_prompt_history_report(entries: &[PromptHistoryEntry], limit: usize) ->
|
||||
}
|
||||
|
||||
fn collect_session_prompt_history(session: &Session) -> Vec<PromptHistoryEntry> {
|
||||
if !session.prompt_history.is_empty() {
|
||||
return session
|
||||
.prompt_history
|
||||
.iter()
|
||||
.map(|entry| PromptHistoryEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
text: entry.text.clone(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let timestamp_ms = session.updated_at_ms;
|
||||
session
|
||||
.messages
|
||||
|
||||
Reference in New Issue
Block a user