yaicli 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yaicli"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "A simple CLI tool to interact with LLM"
5
5
  authors = [{ name = "belingud", email = "im.victor@qq.com" }]
6
6
  readme = "README.md"
yaicli/chat_manager.py ADDED
@@ -0,0 +1,263 @@
1
+ import json
2
+ import time
3
+ from abc import ABC, abstractmethod
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, TypedDict
7
+
8
+ from rich import get_console
9
+ from rich.console import Console
10
+
11
+ from yaicli.config import Config
12
+
13
+
14
+ class ChatsMap(TypedDict):
15
+ title: Dict[str, dict]
16
+ index: Dict[int, dict]
17
+
18
+
19
+ class ChatManager(ABC):
20
+ """Abstract base class that defines the chat manager interface"""
21
+
22
+ @abstractmethod
23
+ def make_chat_title(self, prompt: Optional[str] = None) -> str:
24
+ """Make a chat title from a given full prompt"""
25
+ pass
26
+
27
+ @abstractmethod
28
+ def save_chat(self, history: List[Dict[str, Any]], title: Optional[str] = None) -> str:
29
+ """Save a chat and return the chat title"""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def list_chats(self) -> List[Dict[str, Any]]:
34
+ """List all saved chats and return the chat list"""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def refresh_chats(self) -> None:
39
+ """Force refresh the chat list"""
40
+ pass
41
+
42
+ @abstractmethod
43
+ def load_chat_by_index(self, index: int) -> Dict[str, Any]:
44
+ """Load a chat by index and return the chat data"""
45
+ pass
46
+
47
+ @abstractmethod
48
+ def load_chat_by_title(self, title: str) -> Dict[str, Any]:
49
+ """Load a chat by title and return the chat data"""
50
+ pass
51
+
52
+ @abstractmethod
53
+ def delete_chat(self, index: int) -> bool:
54
+ """Delete a chat by index and return success status"""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def validate_chat_index(self, index: int) -> bool:
59
+ """Validate a chat index and return success status"""
60
+ pass
61
+
62
+
63
+ class FileChatManager(ChatManager):
64
+ """File system based chat manager implementation"""
65
+
66
+ def __init__(self, config: Config, console: Optional[Console] = None):
67
+ self.config = config
68
+ self.chat_dir = Path(self.config["CHAT_HISTORY_DIR"])
69
+ self.chat_dir.mkdir(parents=True, exist_ok=True)
70
+ self.max_saved_chats = self.config["MAX_SAVED_CHATS"]
71
+ self._chats_map: Optional[ChatsMap] = None # Cache for chat map
72
+ self.console = console or get_console()
73
+
74
+ @property
75
+ def chats_map(self) -> ChatsMap:
76
+ """Get the map of chats, loading from disk only when needed"""
77
+ if self._chats_map is None:
78
+ self._load_chats()
79
+ return self._chats_map or {"index": {}, "title": {}}
80
+
81
+ def make_chat_title(self, prompt: Optional[str] = None) -> str:
82
+ """Make a chat title from a given full prompt"""
83
+ if prompt:
84
+ return prompt[:100]
85
+ else:
86
+ return f"Chat-{int(time.time())}"
87
+
88
+ def validate_chat_index(self, index: int) -> bool:
89
+ """Validate a chat index and return success status"""
90
+ return index > 0 and index in self.chats_map["index"]
91
+
92
+ def refresh_chats(self) -> None:
93
+ """Force refresh the chat list from disk"""
94
+ self._load_chats()
95
+
96
+ def _parse_filename(self, chat_file: Path, index: int) -> Dict[str, Any]:
97
+ """Parse a chat filename and extract metadata"""
98
+ # filename: "20250421-214005-title-meaning of life"
99
+ filename = chat_file.stem
100
+ parts = filename.split("-")
101
+ title_str_len = 6 # "title-" marker length
102
+
103
+ # Check if the filename has the expected format
104
+ if len(parts) >= 4 and "title" in parts:
105
+ str_title_index = filename.find("title")
106
+ if str_title_index == -1:
107
+ # If "title" is not found, use full filename as the title
108
+ # Just in case, fallback to use fullname, but this should never happen when `len(parts) >= 4 and "title" in parts`
109
+ str_title_index = 0
110
+ title_str_len = 0
111
+
112
+ # "20250421-214005-title-meaning of life" ==> "meaning of life"
113
+ title = filename[str_title_index + title_str_len :]
114
+ date_ = parts[0]
115
+ time_ = parts[1]
116
+ # Format date
117
+ date_str = f"{date_[:4]}-{date_[4:6]}-{date_[6:]} {time_[:2]}:{time_[2:4]}"
118
+
119
+ # Calculate timestamp from date parts
120
+ try:
121
+ date_time_str = f"{date_}{time_}"
122
+ timestamp = int(datetime.strptime(date_time_str, "%Y%m%d%H%M%S").timestamp())
123
+ except ValueError:
124
+ timestamp = 0
125
+ else:
126
+ # Fallback for files that don't match expected format
127
+ title = filename
128
+ date_str = ""
129
+ timestamp = 0
130
+
131
+ # The actual title is stored in the JSON file, so we'll use that when loading
132
+ # This is just for the initial listing before the file is opened
133
+ return {
134
+ "index": index,
135
+ "path": str(chat_file),
136
+ "title": title,
137
+ "date": date_str,
138
+ "timestamp": timestamp,
139
+ }
140
+
141
+ def _load_chats(self) -> None:
142
+ """Load chats from disk into memory"""
143
+ chat_files = sorted(list(self.chat_dir.glob("*.json")), reverse=True)
144
+ chats_map: ChatsMap = {"title": {}, "index": {}}
145
+
146
+ for i, chat_file in enumerate(chat_files[: self.max_saved_chats]):
147
+ try:
148
+ info = self._parse_filename(chat_file, i + 1)
149
+ chats_map["title"][info["title"]] = info
150
+ chats_map["index"][i + 1] = info
151
+ except Exception as e:
152
+ # Log the error but continue processing other files
153
+ self.console.print(f"Error parsing session file {chat_file}: {e}", style="dim")
154
+ continue
155
+
156
+ self._chats_map = chats_map
157
+
158
+ def list_chats(self) -> List[Dict[str, Any]]:
159
+ """List all saved chats and return the chat list"""
160
+ return list(self.chats_map["index"].values())
161
+
162
+ def save_chat(self, history: List[Dict[str, Any]], title: Optional[str] = None) -> str:
163
+ """Save chat history to the file system, overwriting existing chats with the same title.
164
+
165
+ If no title is provided, the chat will be saved with a default title.
166
+ The default title is "Chat-{current timestamp}".
167
+
168
+ Args:
169
+ history (List[Dict[str, Any]]): The chat history to save
170
+ title (Optional[str]): The title of the chat provided by the user
171
+
172
+ Returns:
173
+ str: The title of the saved chat
174
+ """
175
+ history = history or []
176
+
177
+ save_title = title or f"Chat-{int(time.time())}"
178
+ save_title = self.make_chat_title(save_title)
179
+
180
+ # Check for existing session with the same title and delete it
181
+ existing_chat = self.chats_map["title"].get(save_title)
182
+ if existing_chat:
183
+ try:
184
+ existing_path = Path(existing_chat["path"])
185
+ existing_path.unlink()
186
+ except OSError as e:
187
+ self.console.print(
188
+ f"Warning: Could not delete existing chat file {existing_chat['path']}: {e}",
189
+ style="dim",
190
+ )
191
+
192
+ timestamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S")
193
+ filename = f"{timestamp}-title-{save_title}.json"
194
+ filepath = self.chat_dir / filename
195
+
196
+ try:
197
+ with open(filepath, "w", encoding="utf-8") as f:
198
+ json.dump({"history": history, "title": save_title}, f, ensure_ascii=False, indent=2)
199
+ # Force refresh the chat list after saving
200
+ self.refresh_chats()
201
+ return save_title
202
+ except Exception as e:
203
+ self.console.print(f"Error saving chat '{save_title}': {e}", style="dim")
204
+ return ""
205
+
206
+ def _load_chat_data(self, chat_info: Optional[Dict[str, Any]]) -> Dict[str, Any]:
207
+ """Common method to load chat data from a chat info dict"""
208
+ if not chat_info:
209
+ return {}
210
+
211
+ try:
212
+ chat_file = Path(chat_info["path"])
213
+ with open(chat_file, "r", encoding="utf-8") as f:
214
+ chat_data = json.load(f)
215
+
216
+ return {
217
+ "title": chat_data.get("title", chat_info["title"]),
218
+ "timestamp": chat_info["timestamp"],
219
+ "history": chat_data.get("history", []),
220
+ }
221
+ except FileNotFoundError:
222
+ self.console.print(f"Chat file not found: {chat_info['path']}", style="dim")
223
+ return {}
224
+ except json.JSONDecodeError as e:
225
+ self.console.print(f"Invalid JSON in chat file {chat_info['path']}: {e}", style="dim")
226
+ return {}
227
+ except Exception as e:
228
+ self.console.print(f"Error loading chat from {chat_info['path']}: {e}", style="dim")
229
+ return {}
230
+
231
+ def load_chat_by_index(self, index: int) -> Dict[str, Any]:
232
+ """Load a chat by index and return the chat data"""
233
+ if not self.validate_chat_index(index):
234
+ return {}
235
+ chat_info = self.chats_map.get("index", {}).get(index)
236
+ return self._load_chat_data(chat_info)
237
+
238
+ def load_chat_by_title(self, title: str) -> Dict[str, Any]:
239
+ """Load a chat by title and return the chat data"""
240
+ chat_info = self.chats_map.get("title", {}).get(title)
241
+ return self._load_chat_data(chat_info)
242
+
243
+ def delete_chat(self, index: int) -> bool:
244
+ """Delete a chat by index and return success status"""
245
+ if not self.validate_chat_index(index):
246
+ return False
247
+
248
+ chat_info = self.chats_map["index"].get(index)
249
+ if not chat_info:
250
+ return False
251
+
252
+ try:
253
+ chat_file = Path(chat_info["path"])
254
+ chat_file.unlink()
255
+ # Force refresh the chat list
256
+ self.refresh_chats()
257
+ return True
258
+ except FileNotFoundError:
259
+ self.console.print(f"Chat file not found: {chat_info['path']}", style="dim")
260
+ return False
261
+ except Exception as e:
262
+ self.console.print(f"Error deleting chat {index}: {e}", style="dim")
263
+ return False
yaicli/cli.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import subprocess
2
2
  import sys
3
+ import time
3
4
  import traceback
4
5
  from os.path import devnull
5
6
  from pathlib import Path
@@ -17,13 +18,18 @@ from rich.panel import Panel
17
18
  from rich.prompt import Prompt
18
19
 
19
20
  from yaicli.api import ApiClient
21
+ from yaicli.chat_manager import ChatManager, FileChatManager
20
22
  from yaicli.config import CONFIG_PATH, Config
21
23
  from yaicli.const import (
22
24
  CHAT_MODE,
23
25
  CMD_CLEAR,
26
+ CMD_DELETE_CHAT,
24
27
  CMD_EXIT,
25
28
  CMD_HISTORY,
29
+ CMD_LIST_CHATS,
30
+ CMD_LOAD_CHAT,
26
31
  CMD_MODE,
32
+ CMD_SAVE_CHAT,
27
33
  DEFAULT_CODE_THEME,
28
34
  DEFAULT_INTERACTIVE_ROUND,
29
35
  DEFAULT_OS_NAME,
@@ -42,15 +48,32 @@ class CLI:
42
48
  HISTORY_FILE = Path("~/.yaicli_history").expanduser()
43
49
 
44
50
  def __init__(
45
- self, verbose: bool = False, api_client: Optional[ApiClient] = None, printer: Optional[Printer] = None
51
+ self,
52
+ verbose: bool = False,
53
+ stdin: Optional[str] = None,
54
+ api_client: Optional[ApiClient] = None,
55
+ printer: Optional[Printer] = None,
56
+ chat_manager: Optional[ChatManager] = None,
46
57
  ):
47
58
  self.verbose = verbose
59
+ self.stdin = stdin
48
60
  self.console = get_console()
49
61
  self.bindings = KeyBindings()
50
62
  self.config: Config = Config(self.console)
51
63
  self.current_mode: str = TEMP_MODE
64
+
52
65
  self.history = []
53
66
  self.interactive_max_history = self.config.get("INTERACTIVE_MAX_HISTORY", DEFAULT_INTERACTIVE_ROUND)
67
+ self.chat_title = None
68
+ self.chat_start_time = None
69
+ self.is_temp_session = True
70
+
71
+ # Get and create chat history directory from configuration
72
+ self.chat_history_dir = Path(self.config["CHAT_HISTORY_DIR"])
73
+ self.chat_history_dir.mkdir(parents=True, exist_ok=True)
74
+
75
+ # Initialize session manager
76
+ self.chat_manager = chat_manager or FileChatManager(self.config)
54
77
 
55
78
  # Detect OS and Shell if set to auto
56
79
  if self.config.get("OS_NAME") == DEFAULT_OS_NAME:
@@ -63,7 +86,7 @@ class CLI:
63
86
  self.console.print(f"Config file path: {CONFIG_PATH}")
64
87
  for key, value in self.config.items():
65
88
  display_value = "****" if key == "API_KEY" and value else value
66
- self.console.print(f" {key:<16}: {display_value}")
89
+ self.console.print(f" {key:<17}: {display_value}")
67
90
  self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", DEFAULT_CODE_THEME)))
68
91
 
69
92
  self.api_client = api_client or ApiClient(self.config, self.console, self.verbose)
@@ -77,19 +100,104 @@ class CLI:
77
100
  self.session = PromptSession(key_bindings=self.bindings)
78
101
  finally:
79
102
  if _origin_stderr:
103
+ sys.stderr.flush()
80
104
  sys.stderr.close()
81
105
  sys.stderr = _origin_stderr
82
106
 
83
107
  def get_prompt_tokens(self) -> List[Tuple[str, str]]:
84
108
  """Return prompt tokens for current mode"""
85
- qmark = "💬" if self.current_mode == CHAT_MODE else "🚀" if self.current_mode == EXEC_MODE else "📝"
86
- return [("class:qmark", f" {qmark} "), ("class:prompt", "> ")]
109
+ mode_icon = "💬" if self.current_mode == CHAT_MODE else "🚀" if self.current_mode == EXEC_MODE else "📝"
110
+ return [("class:qmark", f" {mode_icon} "), ("class:prompt", "> ")]
87
111
 
88
112
  def _check_history_len(self) -> None:
89
113
  """Check history length and remove oldest messages if necessary"""
90
114
  target_len = self.interactive_max_history * 2
91
115
  if len(self.history) > target_len:
92
116
  self.history = self.history[-target_len:]
117
+ if self.verbose:
118
+ self.console.print(f"History trimmed to {target_len} messages.", style="dim")
119
+
120
+ def _save_chat(self, title: Optional[str] = None) -> None:
121
+ """Save current chat history to a file using session manager."""
122
+ saved_title = self.chat_manager.save_chat(self.history, title)
123
+
124
+ if not saved_title:
125
+ self.console.print("Failed to save chat.", style="bold red")
126
+ return
127
+
128
+ # Session list will be refreshed automatically by the save method
129
+ self.console.print(f"Chat saved as: {saved_title}", style="bold green")
130
+
131
+ # If this was a temporary session, mark it as non-temporary now that it's saved
132
+ if self.is_temp_session:
133
+ self.is_temp_session = False
134
+ self.chat_title = saved_title
135
+ self.chat_start_time = int(time.time())
136
+ self.console.print(
137
+ "Session is now marked as persistent and will be auto-saved on exit.", style="bold green"
138
+ )
139
+
140
+ def _list_chats(self) -> None:
141
+ """List all saved chat sessions using session manager."""
142
+ chats = self.chat_manager.list_chats()
143
+
144
+ if not chats:
145
+ self.console.print("No saved chats found.", style="yellow")
146
+ return
147
+
148
+ self.console.print("Saved Chats:", style="bold underline")
149
+ for chat in chats:
150
+ index = chat["index"]
151
+ title = chat["title"]
152
+ date = chat.get("date", "")
153
+
154
+ if date:
155
+ self.console.print(f"[dim]{index}.[/dim] [bold blue]{title}[/bold blue] - {date}")
156
+ else:
157
+ self.console.print(f"[dim]{index}.[/dim] [bold blue]{title}[/bold blue]")
158
+
159
+ def _refresh_chats(self) -> None:
160
+ """Force refresh the chat list."""
161
+ self.chat_manager.refresh_chats()
162
+
163
+ def _load_chat_by_index(self, index: int) -> bool:
164
+ """Load a chat session by its index using session manager."""
165
+ if not self.chat_manager.validate_chat_index(index):
166
+ self.console.print("Invalid chat index.", style="bold red")
167
+ return False
168
+
169
+ chat_data = self.chat_manager.load_chat_by_index(index)
170
+
171
+ if not chat_data:
172
+ self.console.print("Invalid chat index or chat not found.", style="bold red")
173
+ return False
174
+
175
+ self.history = chat_data.get("history", [])
176
+ self.chat_title = chat_data.get("title")
177
+ self.chat_start_time = chat_data.get("timestamp", int(time.time()))
178
+ self.is_temp_session = False
179
+
180
+ self.console.print(f"Loaded chat: {self.chat_title}", style="bold green")
181
+ return True
182
+
183
+ def _delete_chat_by_index(self, index: int) -> bool:
184
+ """Delete a chat session by its index using session manager."""
185
+ if not self.chat_manager.validate_chat_index(index):
186
+ self.console.print("Invalid chat index.", style="bold red")
187
+ return False
188
+
189
+ chat_data = self.chat_manager.load_chat_by_index(index)
190
+
191
+ if not chat_data:
192
+ self.console.print("Invalid chat index or chat not found.", style="bold red")
193
+ return False
194
+
195
+ if self.chat_manager.delete_chat(index):
196
+ self.console.print(f"Deleted chat: {chat_data['title']}", style="bold green")
197
+ return True
198
+ else:
199
+ self.console.print(f"Failed to delete chat: {chat_data['title']}", style="bold red")
200
+ return False
93
201
 
94
202
  def _handle_special_commands(self, user_input: str) -> Optional[bool]:
95
203
  """Handle special command return: True-continue loop, False-exit loop, None-non-special command"""
@@ -105,17 +213,52 @@ class CLI:
105
213
  self.console.print("History is empty.", style="yellow")
106
214
  else:
107
215
  self.console.print("Chat History:", style="bold underline")
108
- code_theme = self.config.get("CODE_THEME", "monokai")
109
216
  for i in range(0, len(self.history), 2):
110
217
  user_msg = self.history[i]
111
218
  assistant_msg = self.history[i + 1] if (i + 1) < len(self.history) else None
112
219
  self.console.print(f"[dim]{i // 2 + 1}[/dim] [bold blue]User:[/bold blue] {user_msg['content']}")
113
220
  if assistant_msg:
114
- md = Markdown(assistant_msg["content"], code_theme=code_theme)
221
+ md = Markdown(assistant_msg["content"], code_theme=self.config["CODE_THEME"])
115
222
  padded_md = Padding(md, (0, 0, 0, 4))
116
223
  self.console.print(" Assistant:", style="bold green")
117
224
  self.console.print(padded_md)
118
225
  return True
226
+
227
+ # Handle /save command - optional title parameter
228
+ if command.startswith(CMD_SAVE_CHAT):
229
+ parts = command.split(maxsplit=1)
230
+ title = parts[1] if len(parts) > 1 else self.chat_title
231
+ self._save_chat(title)
232
+ return True
233
+
234
+ # Handle /load command - requires index parameter
235
+ if command.startswith(CMD_LOAD_CHAT):
236
+ parts = command.split(maxsplit=1)
237
+ if len(parts) == 2 and parts[1].isdigit():
238
+ # Try to parse as an index first
239
+ index = int(parts[1])
240
+ self._load_chat_by_index(index=index)
241
+ else:
242
+ self.console.print(f"Usage: {CMD_LOAD_CHAT} <index>", style="yellow")
243
+ self._list_chats()
244
+ return True
245
+
246
+ # Handle /delete command - requires index parameter
247
+ if command.startswith(CMD_DELETE_CHAT):
248
+ parts = command.split(maxsplit=1)
249
+ if len(parts) == 2 and parts[1].isdigit():
250
+ index = int(parts[1])
251
+ self._delete_chat_by_index(index=index)
252
+ else:
253
+ self.console.print(f"Usage: {CMD_DELETE_CHAT} <index>", style="yellow")
254
+ self._list_chats()
255
+ return True
256
+
257
+ # Handle /list command to list saved chats
258
+ if command == CMD_LIST_CHATS:
259
+ self._list_chats()
260
+ return True
261
+
119
262
  # Handle /mode command
120
263
  if command.startswith(CMD_MODE):
121
264
  parts = command.split(maxsplit=1)
@@ -176,9 +319,10 @@ class CLI:
176
319
  def get_system_prompt(self) -> str:
177
320
  """Return system prompt for current mode"""
178
321
  prompt_template = SHELL_PROMPT if self.current_mode == EXEC_MODE else DEFAULT_PROMPT
179
- return prompt_template.format(
180
- _os=self.config.get("OS_NAME", "Unknown OS"), _shell=self.config.get("SHELL_NAME", "Unknown Shell")
181
- )
322
+ stdin = f"\n\nSTDIN\n{self.stdin}" if self.stdin else ""
323
+ if self.verbose and stdin:
324
+ self.console.print("Added STDIN to prompt", style="dim")
325
+ return prompt_template.format(_os=self.config["OS_NAME"], _shell=self.config["SHELL_NAME"]) + stdin
182
326
 
183
327
  def _build_messages(self, user_input: str) -> List[dict]:
184
328
  """Build the list of messages for the API call."""
@@ -251,9 +395,25 @@ class CLI:
251
395
  style="bold cyan",
252
396
  )
253
397
  self.console.print("Welcome to YAICLI!", style="bold")
398
+
399
+ # Display session type
400
+ if self.is_temp_session:
401
+ self.console.print("Current: [bold yellow]Temporary Session[/bold yellow] (use /save to make persistent)")
402
+ else:
403
+ self.console.print(
404
+ f"Current: [bold green]Persistent Session[/bold green]{f': {self.chat_title}' if self.chat_title else ''}"
405
+ )
406
+
254
407
  self.console.print("Press [bold yellow]TAB[/bold yellow] to switch mode")
255
408
  self.console.print(f"{CMD_CLEAR:<19}: Clear chat history")
256
409
  self.console.print(f"{CMD_HISTORY:<19}: Show chat history")
410
+ self.console.print(f"{CMD_LIST_CHATS:<19}: List saved chats")
411
+ save_cmd = f"{CMD_SAVE_CHAT} <title>"
412
+ self.console.print(f"{save_cmd:<19}: Save current chat")
413
+ load_cmd = f"{CMD_LOAD_CHAT} <index>"
414
+ self.console.print(f"{load_cmd:<19}: Load a saved chat")
415
+ delete_cmd = f"{CMD_DELETE_CHAT} <index>"
416
+ self.console.print(f"{delete_cmd:<19}: Delete a saved chat")
257
417
  cmd_exit = f"{CMD_EXIT}|Ctrl+D|Ctrl+C"
258
418
  self.console.print(f"{cmd_exit:<19}: Exit")
259
419
  cmd_mode = f"{CMD_MODE} {CHAT_MODE}|{EXEC_MODE}"
@@ -264,7 +424,7 @@ class CLI:
264
424
  self.prepare_chat_loop()
265
425
  self._print_welcome_message()
266
426
  while True:
267
- self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", "monokai")))
427
+ self.console.print(Markdown("---", code_theme=self.config["CODE_THEME"]))
268
428
  try:
269
429
  user_input = self.session.prompt(self.get_prompt_tokens)
270
430
  user_input = user_input.strip()
@@ -279,6 +439,11 @@ class CLI:
279
439
  break
280
440
  except (KeyboardInterrupt, EOFError):
281
441
  break
442
+
443
+ # Auto-save chat history when exiting if there are messages and not a temporary session
444
+ if not self.is_temp_session:
445
+ self._save_chat(self.chat_title)
446
+
282
447
  self.console.print("\nExiting YAICLI... Goodbye!", style="bold green")
283
448
 
284
449
  def _run_once(self, prompt_arg: str, is_shell_mode: bool) -> None:
@@ -310,6 +475,9 @@ class CLI:
310
475
  except Exception as e:
311
476
  self.console.print(f"[red]Error initializing prompt session history: {e}[/red]")
312
477
  self.session = PromptSession(key_bindings=self.bindings)
478
+ if self.chat_title:
479
+ chat_info = self.chat_manager.load_chat_by_title(self.chat_title)
480
+ self.history = chat_info.get("history", [])
313
481
 
314
482
  def _setup_key_bindings(self) -> None:
315
483
  """Setup keyboard shortcuts (e.g., TAB for mode switching)."""
@@ -325,6 +493,20 @@ class CLI:
325
493
  self.console.print("[bold red]Error:[/bold red] API key not found. Cannot start chat mode.")
326
494
  return
327
495
  self.current_mode = CHAT_MODE
496
+
497
+ # Only set chat start time and title if a prompt is provided
498
+ if prompt:
499
+ self.chat_start_time = int(time.time())
500
+ self.chat_title = self.chat_manager.make_chat_title(prompt)
501
+ self.console.print(f"Chat title: [bold]{self.chat_title}[/bold]")
502
+ self.is_temp_session = False
503
+ else:
504
+ # Mark as temporary session if no prompt
505
+ self.is_temp_session = True
506
+ self.console.print(
507
+ "Starting a temporary chat session (will not be saved automatically)", style="yellow"
508
+ )
509
+
328
510
  self._run_repl()
329
511
  elif prompt:
330
512
  self._run_once(prompt, shell)