Skip to content

Commit 2a7559f

Browse files
feat: add timestamp for new user prompts (#2532)
1 parent a5a21ab commit 2a7559f

File tree

4 files changed

+54
-8
lines changed

4 files changed

+54
-8
lines changed

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.

crates/chat-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ bstr.workspace = true
4545
bytes.workspace = true
4646
camino.workspace = true
4747
cfg-if.workspace = true
48+
chrono.workspace = true
4849
clap.workspace = true
4950
clap_complete.workspace = true
5051
clap_complete_fig.workspace = true

crates/chat-cli/src/cli/chat/conversation.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ use crate::cli::chat::ChatError;
6767
use crate::mcp_client::Prompt;
6868
use crate::os::Os;
6969

70-
const CONTEXT_ENTRY_START_HEADER: &str = "--- CONTEXT ENTRY BEGIN ---\n";
71-
const CONTEXT_ENTRY_END_HEADER: &str = "--- CONTEXT ENTRY END ---\n\n";
70+
pub const CONTEXT_ENTRY_START_HEADER: &str = "--- CONTEXT ENTRY BEGIN ---\n";
71+
pub const CONTEXT_ENTRY_END_HEADER: &str = "--- CONTEXT ENTRY END ---\n\n";
7272

7373
#[derive(Debug, Clone, Serialize, Deserialize)]
7474
pub struct HistoryEntry {

crates/chat-cli/src/cli/chat/message.rs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use std::collections::HashMap;
22
use std::env;
33

4+
use chrono::{
5+
DateTime,
6+
Utc,
7+
};
48
use serde::{
59
Deserialize,
610
Serialize,
@@ -14,6 +18,10 @@ use super::consts::{
1418
MAX_CURRENT_WORKING_DIRECTORY_LEN,
1519
MAX_USER_MESSAGE_SIZE,
1620
};
21+
use super::conversation::{
22+
CONTEXT_ENTRY_END_HEADER,
23+
CONTEXT_ENTRY_START_HEADER,
24+
};
1725
use super::tools::{
1826
InvokeOutput,
1927
OutputKind,
@@ -46,6 +54,7 @@ pub struct UserMessage {
4654
pub additional_context: String,
4755
pub env_context: UserEnvContext,
4856
pub content: UserMessageContent,
57+
pub timestamp: DateTime<Utc>,
4958
pub images: Option<Vec<ImageBlock>>,
5059
}
5160

@@ -101,6 +110,7 @@ impl UserMessage {
101110
pub fn new_prompt(prompt: String) -> Self {
102111
Self {
103112
images: None,
113+
timestamp: Utc::now(),
104114
additional_context: String::new(),
105115
env_context: UserEnvContext::generate_new(),
106116
content: UserMessageContent::Prompt { prompt },
@@ -110,6 +120,7 @@ impl UserMessage {
110120
pub fn new_cancelled_tool_uses<'a>(prompt: Option<String>, tool_use_ids: impl Iterator<Item = &'a str>) -> Self {
111121
Self {
112122
images: None,
123+
timestamp: Utc::now(),
113124
additional_context: String::new(),
114125
env_context: UserEnvContext::generate_new(),
115126
content: UserMessageContent::CancelledToolUses {
@@ -130,6 +141,7 @@ impl UserMessage {
130141
pub fn new_tool_use_results(results: Vec<ToolUseResult>) -> Self {
131142
Self {
132143
additional_context: String::new(),
144+
timestamp: Utc::now(),
133145
env_context: UserEnvContext::generate_new(),
134146
content: UserMessageContent::ToolUseResults {
135147
tool_use_results: results,
@@ -141,6 +153,7 @@ impl UserMessage {
141153
pub fn new_tool_use_results_with_images(results: Vec<ToolUseResult>, images: Vec<ImageBlock>) -> Self {
142154
Self {
143155
additional_context: String::new(),
156+
timestamp: Utc::now(),
144157
env_context: UserEnvContext::generate_new(),
145158
content: UserMessageContent::ToolUseResults {
146159
tool_use_results: results,
@@ -267,13 +280,25 @@ impl UserMessage {
267280

268281
/// Returns a formatted [String] containing [Self::additional_context] and [Self::prompt].
269282
fn content_with_context(&self) -> String {
270-
match (self.additional_context.is_empty(), self.prompt()) {
283+
// Format the time with iso8601 format using Z, e.g. 2025-08-08T17:43:28.672Z
284+
let timestamp = self.timestamp.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
285+
286+
let prompt_with_timestamp = self.prompt().map(|p| {
287+
format!(
288+
"{}Current UTC time: {}{}{}{}{}",
289+
CONTEXT_ENTRY_START_HEADER,
290+
timestamp,
291+
CONTEXT_ENTRY_END_HEADER,
292+
USER_ENTRY_START_HEADER,
293+
p,
294+
USER_ENTRY_END_HEADER
295+
)
296+
});
297+
298+
match (self.additional_context.is_empty(), prompt_with_timestamp) {
271299
// Only add special delimiters if we have both a prompt and additional context
272-
(false, Some(prompt)) => format!(
273-
"{} {}{}{}",
274-
self.additional_context, USER_ENTRY_START_HEADER, prompt, USER_ENTRY_END_HEADER
275-
),
276-
(true, Some(prompt)) => prompt.to_string(),
300+
(false, Some(prompt)) => format!("{}\n{}", self.additional_context, prompt),
301+
(true, Some(prompt)) => prompt,
277302
_ => self.additional_context.clone(),
278303
}
279304
.trim()
@@ -521,4 +546,23 @@ mod tests {
521546
assert!(env_state.operating_system.as_ref().is_some_and(|os| !os.is_empty()));
522547
println!("{env_state:?}");
523548
}
549+
550+
#[test]
551+
fn test_user_input_message_timestamp_formatting() {
552+
let msg = UserMessage::new_prompt("hello world".to_string());
553+
554+
let msgs = [
555+
msg.clone().into_user_input_message(None, &HashMap::new()),
556+
msg.clone().into_history_entry(),
557+
];
558+
559+
for m in msgs {
560+
m.content.contains(CONTEXT_ENTRY_START_HEADER);
561+
m.content.contains("Current UTC time");
562+
m.content.contains(CONTEXT_ENTRY_END_HEADER);
563+
m.content.contains(USER_ENTRY_START_HEADER);
564+
m.content.contains("hello world");
565+
m.content.contains(USER_ENTRY_END_HEADER);
566+
}
567+
}
524568
}

0 commit comments

Comments
 (0)