Skip to content

Commit 0cab564

Browse files
committed
feat: Added persistent history
closes: #46 Signed-off-by: Vaibhav Sapate <sapatevaibhav96@gmail.com>
1 parent b21afb1 commit 0cab564

7 files changed

Lines changed: 133 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A cross-platform, AI-powered terminal assistant with custom commands, local API
1515
- Terminal-like UI with command history and autocompletion
1616
- AI-powered responses for natural language queries
1717
- Custom shell command execution (with sudo/password support)
18+
- **Built-in text editors: nano and vim with full modal editing support**
1819
- Secure local storage for API keys
1920
- Cross-platform: Linux, Windows, macOS (experimental)
2021
- Colorful output and user-friendly design

TODO

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@ SCRAPED: change command to change the API key # Scraped as we have new settings
1111
DONE: Add support to various AI models like gemini, groq, etc
1212
DONE: add markdown support
1313
DONE: clickable links
14+
DONE: Add support for nano/vim
15+
DONE: Persistent command history (saved to ~/.term_history)
16+
17+
TODO:
1418
add proper updates to the command like sync/update (for progress)
15-
handle nano/vim
1619
write tests for each of above
17-
Persistent history
1820
API key in build
1921
change model to change the model
2022
A faster sudo command
2123
focus on infutfield in password field each time and after closing on terminal
2224
minimal ls command output
23-
Implement context
25+
Implement context (history knowledge)
26+
Update Welcome screen when there isn't any API key set
27+
Implement landing page with onboarding
2428
handle failing commands

src-tauri/src/commands/history.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
4+
fn get_history_file_path() -> Result<PathBuf, String> {
5+
let home_dir = dirs::home_dir()
6+
.ok_or_else(|| "Failed to get home directory".to_string())?;
7+
8+
Ok(home_dir.join(".term_history"))
9+
}
10+
11+
#[tauri::command]
12+
pub fn save_command_to_history(command: String) -> Result<(), String> {
13+
let history_file = get_history_file_path()?;
14+
15+
// Read existing history
16+
let mut history = if history_file.exists() {
17+
fs::read_to_string(&history_file)
18+
.map_err(|e| format!("Failed to read history file: {}", e))?
19+
} else {
20+
String::new()
21+
};
22+
23+
// Append new command with newline
24+
if !history.is_empty() && !history.ends_with('\n') {
25+
history.push('\n');
26+
}
27+
history.push_str(&command);
28+
history.push('\n');
29+
30+
// Write back to file
31+
fs::write(&history_file, history)
32+
.map_err(|e| format!("Failed to write history file: {}", e))?;
33+
34+
Ok(())
35+
}
36+
37+
#[tauri::command]
38+
pub fn load_command_history() -> Result<Vec<String>, String> {
39+
let history_file = get_history_file_path()?;
40+
41+
if !history_file.exists() {
42+
return Ok(Vec::new());
43+
}
44+
45+
let content = fs::read_to_string(&history_file)
46+
.map_err(|e| format!("Failed to read history file: {}", e))?;
47+
48+
let history: Vec<String> = content
49+
.lines()
50+
.filter(|line| !line.trim().is_empty())
51+
.map(|line| line.to_string())
52+
.collect();
53+
54+
Ok(history)
55+
}
56+
57+
#[tauri::command]
58+
pub fn clear_command_history() -> Result<(), String> {
59+
let history_file = get_history_file_path()?;
60+
61+
if history_file.exists() {
62+
fs::remove_file(&history_file)
63+
.map_err(|e| format!("Failed to clear history file: {}", e))?;
64+
}
65+
66+
Ok(())
67+
}

src-tauri/src/commands/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
pub mod ai;
22
pub mod api_key;
3-
mod files;
3+
pub mod files;
4+
pub mod history;
45
pub mod settings;
56
pub mod shell;
67

78
pub use ai::*;
89
pub use api_key::*;
910
pub use files::*;
11+
pub use history::*;
1012
pub use settings::*;
1113
pub use shell::*;

src-tauri/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ fn main() {
1616
commands::shell::change_directory,
1717
commands::shell::read_file_for_editor,
1818
commands::shell::write_file_from_editor,
19+
commands::history::save_command_to_history,
20+
commands::history::load_command_history,
21+
commands::history::clear_command_history,
22+
commands::files::read_file,
1923
commands::ai::ask_llm,
2024
commands::api_key::save_api_key,
2125
commands::api_key::get_api_key,

src/hooks/useCommandHistory.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect } from 'react';
2+
import { invoke } from '@tauri-apps/api/core';
23

34
const useCommandHistory = () => {
45
const [commandHistory, setCommandHistory] = useState<string[]>([]);
56
const [historyIndex, setHistoryIndex] = useState(-1);
7+
const [isLoaded, setIsLoaded] = useState(false);
68

7-
const addToHistory = (command: string) => {
9+
// Load history from file on mount
10+
useEffect(() => {
11+
const loadHistory = async () => {
12+
try {
13+
const history = await invoke<string[]>('load_command_history');
14+
setCommandHistory(history);
15+
setIsLoaded(true);
16+
} catch (error) {
17+
console.error('Failed to load command history:', error);
18+
setIsLoaded(true);
19+
}
20+
};
21+
22+
loadHistory();
23+
}, []);
24+
25+
const addToHistory = async (command: string) => {
26+
// Add to local state
827
setCommandHistory(prev => [...prev, command]);
928
setHistoryIndex(-1);
29+
30+
// Save to file
31+
try {
32+
await invoke('save_command_to_history', { command });
33+
} catch (error) {
34+
console.error('Failed to save command to history:', error);
35+
}
1036
};
1137

1238
const navigateHistory = (direction: 'up' | 'down'): string | undefined => {
@@ -29,11 +55,24 @@ const useCommandHistory = () => {
2955
return undefined;
3056
};
3157

58+
const clearHistory = async () => {
59+
setCommandHistory([]);
60+
setHistoryIndex(-1);
61+
62+
try {
63+
await invoke('clear_command_history');
64+
} catch (error) {
65+
console.error('Failed to clear command history:', error);
66+
}
67+
};
68+
3269
return {
3370
commandHistory,
3471
historyIndex,
72+
isLoaded,
3573
addToHistory,
36-
navigateHistory
74+
navigateHistory,
75+
clearHistory
3776
};
3877
};
3978

src/utils/intentParser.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ export type Intent =
88
export function parseInput(input: string): Intent {
99
const trimmed = input.trim();
1010

11-
// File viewing: "cat abc.txt", "view abc.txt"
12-
if (/^(view|show|cat)\s+[\w./-]+\.txt$/i.test(trimmed)) {
13-
const filename = trimmed.split(' ').pop()!;
14-
return { type: 'file_view', filename };
11+
// File viewing: "cat abc.txt", "view abc.txt", "cat filename"
12+
if (/^(view|show|cat)\s+[\w./-]+/i.test(trimmed)) {
13+
const parts = trimmed.split(/\s+/);
14+
if (parts.length >= 2) {
15+
const filename = parts.slice(1).join(' ');
16+
return { type: 'file_view', filename };
17+
}
1518
}
1619

1720
// File summary: "summarize abc.txt", "give summary of abc.txt"
18-
if (/summar(y|ise|ize).+\.txt/i.test(trimmed)) {
19-
const match = trimmed.match(/(?:of|about|on)\s+([\w./-]+\.txt)/i);
21+
if (/summar(y|ise|ize).+/i.test(trimmed)) {
22+
const match = trimmed.match(/(?:of|about|on)\s+([\w./-]+)/i);
2023
if (match) return { type: 'file_summary', filename: match[1] };
2124
}
2225

0 commit comments

Comments
 (0)