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 +1 -1
- yaicli/chat_manager.py +263 -0
- yaicli/cli.py +192 -10
- yaicli/config.py +34 -64
- yaicli/const.py +19 -3
- yaicli/entry.py +24 -8
- {yaicli-0.1.0.dist-info → yaicli-0.2.0.dist-info}/METADATA +214 -99
- yaicli-0.2.0.dist-info/RECORD +16 -0
- yaicli-0.1.0.dist-info/RECORD +0 -15
- {yaicli-0.1.0.dist-info → yaicli-0.2.0.dist-info}/WHEEL +0 -0
- {yaicli-0.1.0.dist-info → yaicli-0.2.0.dist-info}/entry_points.txt +0 -0
- {yaicli-0.1.0.dist-info → yaicli-0.2.0.dist-info}/licenses/LICENSE +0 -0
pyproject.toml
CHANGED
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,
|
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:<
|
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
|
-
|
86
|
-
return [("class:qmark", f" {
|
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=
|
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
|
-
|
180
|
-
|
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
|
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)
|