Skip to content

Commit 60a8ca9

Browse files
committed
feat: add chrono dependency and implement synthesis saving functionality
1 parent ef5ccce commit 60a8ca9

File tree

7 files changed

+211
-36
lines changed

7 files changed

+211
-36
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ futures = "0.3.30"
3232
signal-hook = "0.3.17"
3333
textwrap = "0.16.1"
3434
unicode-width = "0.1.13"
35+
chrono = { version = "0.4.38", features = ["serde"] }
3536

3637
[workspace.lints.rust]
3738
dead_code = "warn"

crates/agentic-core/src/cloud.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ pub enum CloudError {
1313
ParseError,
1414

1515
#[error("The cloud provider returned an unexpected error: {status}: {text}")]
16-
ApiError {
17-
status: u16,
18-
text: String,
19-
},
16+
ApiError { status: u16, text: String },
2017

2118
#[error(transparent)]
2219
RequestError(#[from] reqwest::Error),
@@ -132,4 +129,4 @@ pub async fn call_cloud_model(
132129
serde_json::from_str(message_content).map_err(|_| CloudError::ParseError)?;
133130

134131
Ok(atomic_note)
135-
}
132+
}

crates/agentic-core/src/orchestrator.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ use serde::Deserialize;
33

44
const ORCHESTRATOR_PROMPT: &str = r#"You are Ruixen, an inquisitive AI partner.
55
6-
**Your Task:**
7-
Generate 3 concise proposals about this query: "{query}"
6+
**CRITICAL INSTRUCTION:**
7+
You MUST generate EXACTLY 3 proposals about this query: "{query}"
88
9-
Each proposal must have TWO parts separated by a dash:
10-
1. A brief context statement (1-2 sentences max)
11-
2. A curious question starting with "I wonder" or "I'm wondering"
9+
**MANDATORY FORMAT FOR EACH PROPOSAL:**
10+
[Context statement] - I wonder [question]?
1211
13-
Keep each proposal under 3 lines when displayed. Be thoughtful but concise.
12+
**RULES - NO EXCEPTIONS:**
13+
1. EVERY proposal MUST have a brief context (1-2 sentences) followed by " - I wonder"
14+
2. EVERY proposal MUST end with a question starting with "I wonder" or "I'm wondering"
15+
3. NO proposals should be just statements or just questions
16+
4. ALWAYS use the exact format: "Context - I wonder/I'm wondering [question]?"
1417
15-
**Format:** Brief context - I wonder question?
18+
**EXAMPLE OF CORRECT FORMAT:**
19+
"Philosophy has debated this for centuries - I wonder what new perspectives we might discover?"
1620
17-
**Output Format:**
21+
**Your EXACT output must be valid JSON:**
1822
{
1923
"proposals": [
20-
"Brief context about the topic - I wonder about this specific aspect?",
21-
"Another brief context - I'm wondering if this related thing?",
22-
"Third brief context - I wonder about this other angle?"
24+
"Brief context statement - I wonder about this specific aspect?",
25+
"Another context statement - I'm wondering if this could be true?",
26+
"Third context statement - I wonder about this different angle?"
2327
]
2428
}
2529
"#;

crates/agentic-tui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ ratatui = { workspace = true }
1515
tokio = { workspace = true }
1616
tracing = { workspace = true }
1717
tracing-subscriber = { workspace = true }
18+
chrono = { workspace = true }

crates/agentic-tui/src/ui/app.rs

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,12 @@ impl App {
209209
let prefix = if is_selected { "> " } else { " " };
210210
let number = format!("{}. ", i + 1);
211211

212-
// Clean up the proposal text - remove template artifacts
212+
// Clean up any remaining context artifacts for display
213213
let proposal_text = proposal
214-
.replace("Context statement: ", "")
215-
.replace("Another context: ", "")
216-
.replace("Third context: ", "")
217-
.replace("Context statement - ", "")
218-
.replace("Another context - ", "")
219-
.replace("Third context - ", "");
214+
.replace("From a scientific perspective, ", "")
215+
.replace("As we explore ", "Exploring ")
216+
.trim()
217+
.to_string();
220218

221219
let style = if is_selected {
222220
self.theme.ratatui_style(Element::Accent)
@@ -460,20 +458,13 @@ impl App {
460458
self.render_coaching_tip_modal(frame, modal_area);
461459
} else if self.mode == AppMode::Complete {
462460
let content = if let Some(note) = &self.cloud_response {
463-
use ratatui::text::{Line, Span};
464-
let title = note.header_tags.join(" • ");
465-
let title_style = self.theme.ratatui_style(Element::Accent);
466-
let body_style = self.theme.ratatui_style(Element::Text);
467-
468-
let text = vec![
469-
Line::from(Span::styled(title, title_style)),
470-
Line::from(""), // Spacer
471-
Line::from(Span::styled(&note.body_text, body_style)),
472-
];
473-
Paragraph::new(text)
461+
// Clean display - only show the synthesis content, hide system metadata
462+
Paragraph::new(note.body_text.as_str())
463+
.style(self.theme.ratatui_style(Element::Text))
474464
} else {
475465
// This case should ideally not be reached if mode is Complete
476466
Paragraph::new("Waiting for synthesis...")
467+
.style(self.theme.ratatui_style(Element::Text))
477468
};
478469

479470
let block = Block::default()
@@ -930,12 +921,33 @@ impl App {
930921
},
931922
AppMode::Complete => match key.code {
932923
KeyCode::Up => {
933-
self.synthesis_scroll = self.synthesis_scroll.saturating_sub(1);
924+
// Save synthesis (positive action)
925+
self.save_synthesis();
926+
self.mode = AppMode::Normal;
927+
self.final_prompt.clear();
928+
self.proposals.clear();
929+
self.current_proposal_index = 0;
930+
self.cloud_response = None;
931+
self.synthesis_scroll = 0;
932+
self.agent_status = AgentStatus::Ready;
934933
}
935934
KeyCode::Down => {
936-
self.synthesis_scroll = self.synthesis_scroll.saturating_add(1);
935+
// Discard synthesis (negative action)
936+
self.mode = AppMode::Chat; // Start new query
937+
self.final_prompt.clear();
938+
self.proposals.clear();
939+
self.current_proposal_index = 0;
940+
self.cloud_response = None;
941+
self.synthesis_scroll = 0;
942+
self.agent_status = AgentStatus::Ready;
943+
self.edit_buffer.clear();
944+
}
945+
KeyCode::Left | KeyCode::Right => {
946+
// Keep horizontal scrolling for long content
947+
// Currently no horizontal scroll implemented
937948
}
938949
KeyCode::Enter | KeyCode::Esc => {
950+
// Fallback: return to normal without saving
939951
self.mode = AppMode::Normal;
940952
self.final_prompt.clear();
941953
self.proposals.clear();
@@ -1006,6 +1018,53 @@ impl App {
10061018
self.edit_buffer.clear();
10071019
}
10081020

1021+
fn save_synthesis(&self) {
1022+
if let Some(note) = &self.cloud_response {
1023+
// Generate markdown content with v0.1.0 metadata structure
1024+
let timestamp = chrono::Utc::now().format("%Y-%m-%d_%H-%M-%S").to_string();
1025+
let filename = format!("synthesis_{}.md", timestamp);
1026+
1027+
// Get the selected proposal text
1028+
let proposal_text = if !self.proposals.is_empty()
1029+
&& self.current_proposal_index < self.proposals.len()
1030+
{
1031+
&self.proposals[self.current_proposal_index]
1032+
} else {
1033+
"No proposal available"
1034+
};
1035+
1036+
// Use proposal text directly since the new prompt ensures proper format
1037+
let clean_proposal = proposal_text;
1038+
1039+
let markdown_content = format!(
1040+
"---\ndate: {}\nprovider: \"OPENROUTER\"\nmodel: \"{}\"\nquery: \"{}\"\nproposal: \"{}\"\ntags: [{}]\n---\n\n# {}\n\n{}\n",
1041+
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
1042+
if self.settings.cloud_model.is_empty() || self.settings.cloud_model == "[SELECT]" {
1043+
"anthropic/claude-3.5-sonnet"
1044+
} else {
1045+
&self.settings.cloud_model
1046+
},
1047+
self.final_prompt.replace("\"", "\\\""),
1048+
clean_proposal.replace("\"", "\\\""),
1049+
note.header_tags.iter().map(|tag| format!("\"{}\"", tag)).collect::<Vec<_>>().join(", "),
1050+
note.header_tags.join(" • "),
1051+
note.body_text
1052+
);
1053+
1054+
// Create Documents/ruixen directory if it doesn't exist
1055+
let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
1056+
let save_dir = format!("{}/Documents/ruixen", home_dir);
1057+
if std::fs::create_dir_all(&save_dir).is_err() {
1058+
// Silent fallback - don't crash the app
1059+
return;
1060+
}
1061+
1062+
let filepath = format!("{}/{}", save_dir, filename);
1063+
// Silent save - don't print debug logs that crash the TUI
1064+
let _ = std::fs::write(&filepath, markdown_content);
1065+
}
1066+
}
1067+
10091068
fn handle_cloud_synthesis(&mut self) {
10101069
// Set status to searching and trigger cloud API call
10111070
self.agent_status = AgentStatus::Searching;

crates/agentic-tui/src/ui/footer.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ pub fn render_footer(
2020
let inner_area = footer_block.inner(area);
2121

2222
let content = match mode {
23+
AppMode::Complete => {
24+
// Save/Discard navigation for synthesis results
25+
Line::from(vec![
26+
Span::raw("[↑] "),
27+
Span::styled("Save Synthesis", theme.ratatui_style(Element::Accent)),
28+
Span::raw(" | "),
29+
Span::raw("[↓] "),
30+
Span::styled(
31+
"Discard & New Query",
32+
theme.ratatui_style(Element::Inactive),
33+
),
34+
])
35+
.alignment(Alignment::Center)
36+
}
2337
AppMode::Chat => {
2438
// Chat input field with cursor
2539
let display_text = if chat_input.is_empty() {

0 commit comments

Comments
 (0)