Skip to content

Commit c4bfb19

Browse files
committed
resolve merge conflict
2 parents 2c959d0 + 36e70d1 commit c4bfb19

File tree

23 files changed

+644
-160
lines changed

23 files changed

+644
-160
lines changed

Cargo.lock

Lines changed: 2 additions & 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ workspace = true
1414
[features]
1515
default = []
1616
wayland = ["arboard/wayland-data-control"]
17-
knowledge = []
1817

1918
[[bin]]
2019
name = "test_mcp_server"
@@ -46,6 +45,7 @@ bstr.workspace = true
4645
bytes.workspace = true
4746
camino.workspace = true
4847
cfg-if.workspace = true
48+
chrono.workspace = true
4949
clap.workspace = true
5050
clap_complete.workspace = true
5151
clap_complete_fig.workspace = true

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use crate::cli::chat::{
1919
ChatSession,
2020
ChatState,
2121
};
22-
#[cfg(feature = "knowledge")]
2322
use crate::database::settings::Setting;
2423
use crate::os::Os;
2524
use crate::util::knowledge_store::KnowledgeStore;
@@ -70,19 +69,10 @@ impl KnowledgeSubcommand {
7069
}
7170

7271
fn is_feature_enabled(os: &Os) -> bool {
73-
// Feature is only available when compiled with the knowledge feature flag
74-
#[cfg(feature = "knowledge")]
75-
{
76-
os.database
77-
.settings
78-
.get_bool(Setting::EnabledKnowledge)
79-
.unwrap_or(false)
80-
}
81-
#[cfg(not(feature = "knowledge"))]
82-
{
83-
let _ = os; // Suppress unused variable warning
84-
false
85-
}
72+
os.database
73+
.settings
74+
.get_bool(Setting::EnabledKnowledge)
75+
.unwrap_or(false)
8676
}
8777

8878
fn write_feature_disabled_message(session: &mut ChatSession) -> Result<(), std::io::Error> {

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ pub mod compact;
33
pub mod context;
44
pub mod editor;
55
pub mod hooks;
6-
#[cfg(feature = "knowledge")]
76
pub mod knowledge;
87
pub mod mcp;
98
pub mod model;
@@ -20,7 +19,6 @@ use compact::CompactArgs;
2019
use context::ContextSubcommand;
2120
use editor::EditorArgs;
2221
use hooks::HooksArgs;
23-
#[cfg(feature = "knowledge")]
2422
use knowledge::KnowledgeSubcommand;
2523
use mcp::McpArgs;
2624
use model::ModelArgs;
@@ -58,9 +56,9 @@ pub enum SlashCommand {
5856
/// Manage context files for the chat session
5957
#[command(subcommand)]
6058
Context(ContextSubcommand),
61-
/// (Beta) Manage knowledge base for persistent context storage
62-
#[cfg(feature = "knowledge")]
63-
#[command(subcommand)]
59+
/// (Beta) Manage knowledge base for persistent context storage. Requires "q settings
60+
/// chat.enableKnowledge true"
61+
#[command(subcommand, hide = true)]
6462
Knowledge(KnowledgeSubcommand),
6563
/// Open $EDITOR (defaults to vi) to compose a prompt
6664
#[command(name = "editor")]
@@ -119,7 +117,6 @@ impl SlashCommand {
119117
})
120118
},
121119
Self::Context(args) => args.execute(os, session).await,
122-
#[cfg(feature = "knowledge")]
123120
Self::Knowledge(subcommand) => subcommand.execute(os, session).await,
124121
Self::PromptEditor(args) => args.execute(session).await,
125122
Self::Compact(args) => args.execute(os, session).await,
@@ -159,7 +156,6 @@ impl SlashCommand {
159156
Self::Agent(_) => "agent",
160157
Self::Profile => "profile",
161158
Self::Context(_) => "context",
162-
#[cfg(feature = "knowledge")]
163159
Self::Knowledge(_) => "knowledge",
164160
Self::PromptEditor(_) => "editor",
165161
Self::Compact(_) => "compact",
@@ -182,7 +178,6 @@ impl SlashCommand {
182178
match self {
183179
SlashCommand::Agent(sub) => Some(sub.name()),
184180
SlashCommand::Context(sub) => Some(sub.name()),
185-
#[cfg(feature = "knowledge")]
186181
SlashCommand::Knowledge(sub) => Some(sub.name()),
187182
SlashCommand::Tools(arg) => arg.subcommand_name(),
188183
SlashCommand::Prompts(arg) => arg.subcommand_name(),

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use super::context::{
3030
ContextManager,
3131
calc_max_context_files_size,
3232
};
33+
use super::line_tracker::FileLineTracker;
3334
use super::message::{
3435
AssistantMessage,
3536
ToolUseResult,
@@ -71,8 +72,8 @@ use crate::cli::chat::cli::model::{
7172
use crate::mcp_client::Prompt;
7273
use crate::os::Os;
7374

74-
const CONTEXT_ENTRY_START_HEADER: &str = "--- CONTEXT ENTRY BEGIN ---\n";
75-
const CONTEXT_ENTRY_END_HEADER: &str = "--- CONTEXT ENTRY END ---\n\n";
75+
pub const CONTEXT_ENTRY_START_HEADER: &str = "--- CONTEXT ENTRY BEGIN ---\n";
76+
pub const CONTEXT_ENTRY_END_HEADER: &str = "--- CONTEXT ENTRY END ---\n\n";
7677

7778
#[derive(Debug, Clone, Serialize, Deserialize)]
7879
pub struct HistoryEntry {
@@ -113,6 +114,11 @@ pub struct ConversationState {
113114
/// Model explicitly selected by the user in this conversation state via `/model`.
114115
#[serde(default, skip_serializing_if = "Option::is_none")]
115116
pub model: Option<ModelInfo>,
117+
/// Used to track agent vs user updates to file modifications.
118+
///
119+
/// Maps from a file path to [FileLineTracker]
120+
#[serde(default)]
121+
pub file_line_tracker: HashMap<String, FileLineTracker>,
116122
}
117123

118124
impl ConversationState {
@@ -167,6 +173,7 @@ impl ConversationState {
167173
latest_summary: None,
168174
agents,
169175
model,
176+
file_line_tracker: HashMap::new(),
170177
}
171178
}
172179

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use serde::{
2+
Deserialize,
3+
Serialize,
4+
};
5+
6+
/// Contains metadata for tracking user and agent contribution metrics for a given file for
7+
/// `fs_write` tool uses.
8+
#[derive(Debug, Clone, Serialize, Deserialize)]
9+
pub struct FileLineTracker {
10+
/// Line count at the end of the last `fs_write`
11+
pub prev_fswrite_lines: usize,
12+
/// Line count before `fs_write` executes
13+
pub before_fswrite_lines: usize,
14+
/// Line count after `fs_write` executes
15+
pub after_fswrite_lines: usize,
16+
/// Whether or not this is the first `fs_write` invocation
17+
pub is_first_write: bool,
18+
}
19+
20+
impl Default for FileLineTracker {
21+
fn default() -> Self {
22+
Self {
23+
prev_fswrite_lines: 0,
24+
before_fswrite_lines: 0,
25+
after_fswrite_lines: 0,
26+
is_first_write: true,
27+
}
28+
}
29+
}
30+
31+
impl FileLineTracker {
32+
pub fn lines_by_user(&self) -> isize {
33+
(self.before_fswrite_lines as isize) - (self.prev_fswrite_lines as isize)
34+
}
35+
36+
pub fn lines_by_agent(&self) -> isize {
37+
(self.after_fswrite_lines as isize) - (self.before_fswrite_lines as isize)
38+
}
39+
}

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
}

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod input_source;
77
mod message;
88
mod parse;
99
use std::path::MAIN_SEPARATOR;
10+
mod line_tracker;
1011
mod parser;
1112
mod prompt;
1213
mod prompt_parser;
@@ -1883,7 +1884,10 @@ impl ChatSession {
18831884
}
18841885
}
18851886

1886-
let invoke_result = tool.tool.invoke(os, &mut self.stdout).await;
1887+
let invoke_result = tool
1888+
.tool
1889+
.invoke(os, &mut self.stdout, &mut self.conversation.file_line_tracker)
1890+
.await;
18871891

18881892
if self.spinner.is_some() {
18891893
queue!(
@@ -1945,6 +1949,33 @@ impl ChatSession {
19451949
tool_telemetry
19461950
.and_modify(|ev| ev.output_token_size = Some(TokenCounter::count_tokens(&result.as_str())));
19471951
}
1952+
1953+
// Send telemetry for agent contribution
1954+
if let Tool::FsWrite(w) = &tool.tool {
1955+
let sanitized_path_str = w.path(os).to_string_lossy().to_string();
1956+
let conversation_id = self.conversation.conversation_id().to_string();
1957+
let message_id = self.conversation.message_id().map(|s| s.to_string());
1958+
if let Some(tracker) = self.conversation.file_line_tracker.get_mut(&sanitized_path_str) {
1959+
let lines_by_agent = tracker.lines_by_agent();
1960+
let lines_by_user = tracker.lines_by_user();
1961+
1962+
os.telemetry
1963+
.send_agent_contribution_metric(
1964+
&os.database,
1965+
conversation_id,
1966+
message_id,
1967+
Some(tool.id.clone()), // Already a String
1968+
Some(tool.name.clone()), // Already a String
1969+
Some(lines_by_agent),
1970+
Some(lines_by_user),
1971+
)
1972+
.await
1973+
.ok();
1974+
1975+
tracker.prev_fswrite_lines = tracker.after_fswrite_lines;
1976+
}
1977+
}
1978+
19481979
tool_results.push(ToolUseResult {
19491980
tool_use_id: tool.id.clone(),
19501981
content: vec![result.into()],

0 commit comments

Comments
 (0)