mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 17:14:49 +08:00
feat: b5-doctor-cmd — batch 5 wave 2
This commit is contained in:
@@ -504,6 +504,10 @@ where
|
|||||||
&self.session
|
&self.session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn session_mut(&mut self) -> &mut Session {
|
||||||
|
&mut self.session
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn fork_session(&self, branch_name: Option<String>) -> Session {
|
pub fn fork_session(&self, branch_name: Option<String>) -> Session {
|
||||||
self.session.fork(branch_name)
|
self.session.fork(branch_name)
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ pub use sandbox::{
|
|||||||
};
|
};
|
||||||
pub use session::{
|
pub use session::{
|
||||||
ContentBlock, ConversationMessage, MessageRole, Session, SessionCompaction, SessionError,
|
ContentBlock, ConversationMessage, MessageRole, Session, SessionCompaction, SessionError,
|
||||||
SessionFork,
|
SessionFork, SessionPromptEntry,
|
||||||
};
|
};
|
||||||
pub use sse::{IncrementalSseParser, SseEvent};
|
pub use sse::{IncrementalSseParser, SseEvent};
|
||||||
pub use stale_branch::{
|
pub use stale_branch::{
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ pub struct SessionFork {
|
|||||||
pub branch_name: Option<String>,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct SessionPersistence {
|
struct SessionPersistence {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -88,6 +95,7 @@ pub struct Session {
|
|||||||
pub compaction: Option<SessionCompaction>,
|
pub compaction: Option<SessionCompaction>,
|
||||||
pub fork: Option<SessionFork>,
|
pub fork: Option<SessionFork>,
|
||||||
pub workspace_root: Option<PathBuf>,
|
pub workspace_root: Option<PathBuf>,
|
||||||
|
pub prompt_history: Vec<SessionPromptEntry>,
|
||||||
persistence: Option<SessionPersistence>,
|
persistence: Option<SessionPersistence>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +109,7 @@ impl PartialEq for Session {
|
|||||||
&& self.compaction == other.compaction
|
&& self.compaction == other.compaction
|
||||||
&& self.fork == other.fork
|
&& self.fork == other.fork
|
||||||
&& self.workspace_root == other.workspace_root
|
&& self.workspace_root == other.workspace_root
|
||||||
|
&& self.prompt_history == other.prompt_history
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +160,7 @@ impl Session {
|
|||||||
compaction: None,
|
compaction: None,
|
||||||
fork: None,
|
fork: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
prompt_history: Vec::new(),
|
||||||
persistence: None,
|
persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,6 +262,7 @@ impl Session {
|
|||||||
branch_name: normalize_optional_string(branch_name),
|
branch_name: normalize_optional_string(branch_name),
|
||||||
}),
|
}),
|
||||||
workspace_root: self.workspace_root.clone(),
|
workspace_root: self.workspace_root.clone(),
|
||||||
|
prompt_history: self.prompt_history.clone(),
|
||||||
persistence: None,
|
persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,6 +306,17 @@ impl Session {
|
|||||||
JsonValue::String(workspace_root_to_string(workspace_root)?),
|
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))
|
Ok(JsonValue::Object(object))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +361,16 @@ impl Session {
|
|||||||
.get("workspace_root")
|
.get("workspace_root")
|
||||||
.and_then(JsonValue::as_str)
|
.and_then(JsonValue::as_str)
|
||||||
.map(PathBuf::from);
|
.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 {
|
Ok(Self {
|
||||||
version,
|
version,
|
||||||
session_id,
|
session_id,
|
||||||
@@ -348,6 +380,7 @@ impl Session {
|
|||||||
compaction,
|
compaction,
|
||||||
fork,
|
fork,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
|
prompt_history,
|
||||||
persistence: None,
|
persistence: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -361,6 +394,7 @@ impl Session {
|
|||||||
let mut compaction = None;
|
let mut compaction = None;
|
||||||
let mut fork = None;
|
let mut fork = None;
|
||||||
let mut workspace_root = None;
|
let mut workspace_root = None;
|
||||||
|
let mut prompt_history = Vec::new();
|
||||||
|
|
||||||
for (line_number, raw_line) in contents.lines().enumerate() {
|
for (line_number, raw_line) in contents.lines().enumerate() {
|
||||||
let line = raw_line.trim();
|
let line = raw_line.trim();
|
||||||
@@ -414,6 +448,13 @@ impl Session {
|
|||||||
object.clone(),
|
object.clone(),
|
||||||
))?);
|
))?);
|
||||||
}
|
}
|
||||||
|
"prompt_history" => {
|
||||||
|
if let Some(entry) =
|
||||||
|
SessionPromptEntry::from_json_opt(&JsonValue::Object(object.clone()))
|
||||||
|
{
|
||||||
|
prompt_history.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
other => {
|
other => {
|
||||||
return Err(SessionError::Format(format!(
|
return Err(SessionError::Format(format!(
|
||||||
"unsupported JSONL record type at line {}: {other}",
|
"unsupported JSONL record type at line {}: {other}",
|
||||||
@@ -433,15 +474,36 @@ impl Session {
|
|||||||
compaction,
|
compaction,
|
||||||
fork,
|
fork,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
|
prompt_history,
|
||||||
persistence: None,
|
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> {
|
fn render_jsonl_snapshot(&self) -> Result<String, SessionError> {
|
||||||
let mut lines = vec![self.meta_record()?.render()];
|
let mut lines = vec![self.meta_record()?.render()];
|
||||||
if let Some(compaction) = &self.compaction {
|
if let Some(compaction) = &self.compaction {
|
||||||
lines.push(compaction.to_jsonl_record()?.render());
|
lines.push(compaction.to_jsonl_record()?.render());
|
||||||
}
|
}
|
||||||
|
lines.extend(
|
||||||
|
self.prompt_history
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.to_jsonl_record().render()),
|
||||||
|
);
|
||||||
lines.extend(
|
lines.extend(
|
||||||
self.messages
|
self.messages
|
||||||
.iter()
|
.iter()
|
||||||
@@ -468,6 +530,25 @@ impl Session {
|
|||||||
Ok(())
|
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> {
|
fn meta_record(&self) -> Result<JsonValue, SessionError> {
|
||||||
let mut object = BTreeMap::new();
|
let mut object = BTreeMap::new();
|
||||||
object.insert(
|
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 {
|
fn message_record(message: &ConversationMessage) -> JsonValue {
|
||||||
let mut object = BTreeMap::new();
|
let mut object = BTreeMap::new();
|
||||||
object.insert("type".to_string(), JsonValue::String("message".to_string()));
|
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| {
|
.map_or(self.runtime.session().updated_at_ms, |duration| {
|
||||||
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
|
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
|
||||||
});
|
});
|
||||||
self.prompt_history.push(PromptHistoryEntry {
|
let entry = PromptHistoryEntry {
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
text: prompt.to_string(),
|
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>) {
|
fn print_prompt_history(&self, count: Option<&str>) {
|
||||||
@@ -3656,10 +3660,27 @@ impl LiveCli {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let entries = if self.prompt_history.is_empty() {
|
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())
|
collect_session_prompt_history(self.runtime.session())
|
||||||
} else {
|
} else {
|
||||||
self.prompt_history.clone()
|
self.prompt_history
|
||||||
|
.iter()
|
||||||
|
.map(|entry| PromptHistoryEntry {
|
||||||
|
timestamp_ms: entry.timestamp_ms,
|
||||||
|
text: entry.text.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session_entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| PromptHistoryEntry {
|
||||||
|
timestamp_ms: entry.timestamp_ms,
|
||||||
|
text: entry.text.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
};
|
};
|
||||||
println!("{}", render_prompt_history_report(&entries, limit));
|
println!("{}", render_prompt_history_report(&entries, limit));
|
||||||
}
|
}
|
||||||
@@ -5145,7 +5166,7 @@ fn write_temp_text_file(
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_HISTORY_LIMIT: usize = 10;
|
const DEFAULT_HISTORY_LIMIT: usize = 20;
|
||||||
|
|
||||||
fn parse_history_count(raw: Option<&str>) -> Result<usize, String> {
|
fn parse_history_count(raw: Option<&str>) -> Result<usize, String> {
|
||||||
let Some(raw) = raw else {
|
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> {
|
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;
|
let timestamp_ms = session.updated_at_ms;
|
||||||
session
|
session
|
||||||
.messages
|
.messages
|
||||||
|
|||||||
Reference in New Issue
Block a user