yaicli 0.3.3__py3-none-any.whl → 0.5.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.
yaicli/cli.py CHANGED
@@ -2,9 +2,9 @@ import subprocess
2
2
  import sys
3
3
  import time
4
4
  import traceback
5
- from os.path import devnull
5
+ from os import devnull
6
6
  from pathlib import Path
7
- from typing import List, Literal, Optional, Tuple
7
+ from typing import Optional, Union
8
8
 
9
9
  import typer
10
10
  from prompt_toolkit import PromptSession, prompt
@@ -16,95 +16,90 @@ from rich.padding import Padding
16
16
  from rich.panel import Panel
17
17
  from rich.prompt import Prompt
18
18
 
19
- from yaicli.api import ApiClient
20
- from yaicli.chat_manager import ChatFileInfo, ChatManager, FileChatManager
21
- from yaicli.config import CONFIG_PATH, Config, cfg
22
- from yaicli.console import get_console
23
- from yaicli.const import (
19
+ from .chat import Chat, FileChatManager, chat_mgr
20
+ from .client import LitellmClient, ChatMessage
21
+ from .config import cfg
22
+ from .console import get_console
23
+ from .const import (
24
24
  CHAT_MODE,
25
25
  CMD_CLEAR,
26
26
  CMD_DELETE_CHAT,
27
27
  CMD_EXIT,
28
+ CMD_HELP,
28
29
  CMD_HISTORY,
29
30
  CMD_LIST_CHATS,
30
31
  CMD_LOAD_CHAT,
31
32
  CMD_MODE,
32
33
  CMD_SAVE_CHAT,
33
- DEFAULT_CODE_THEME,
34
- DEFAULT_INTERACTIVE_ROUND,
34
+ CONFIG_PATH,
35
35
  DEFAULT_OS_NAME,
36
36
  DEFAULT_SHELL_NAME,
37
37
  EXEC_MODE,
38
+ HISTORY_FILE,
38
39
  TEMP_MODE,
39
40
  DefaultRoleNames,
40
41
  )
41
- from yaicli.history import LimitedFileHistory
42
- from yaicli.printer import Printer
43
- from yaicli.roles import RoleManager
44
- from yaicli.utils import detect_os, detect_shell, filter_command
42
+ from .exceptions import ChatSaveError
43
+ from .history import LimitedFileHistory
44
+ from .printer import Printer
45
+ from .role import Role, RoleManager, role_mgr
46
+ from .utils import detect_os, detect_shell, filter_command
45
47
 
46
48
 
47
49
  class CLI:
48
- HISTORY_FILE = Path("~/.yaicli_history").expanduser()
49
-
50
50
  def __init__(
51
51
  self,
52
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,
57
- role: Optional[str] = None,
53
+ role: str = DefaultRoleNames.DEFAULT,
54
+ chat_manager: Optional[FileChatManager] = None,
55
+ role_manager: Optional[RoleManager] = None,
56
+ client=None,
58
57
  ):
59
- # General settings
60
- self.verbose = verbose
61
- self.stdin = stdin
58
+ self.verbose: bool = verbose
59
+ # --role can specify a role when enter interactive chat
60
+ # TAB will switch between role and shell
61
+ self.init_role: str = role
62
+ self.role_name: str = role
63
+
62
64
  self.console = get_console()
63
- self.bindings = KeyBindings()
64
- self.config: Config = cfg
65
- self.current_mode: str = TEMP_MODE
66
- self.role: str = role or DefaultRoleNames.DEFAULT.value
65
+ self.chat_manager = chat_manager or chat_mgr
66
+ self.role_manager = role_manager or role_mgr
67
+ self.role: Role = self.role_manager.get_role(self.role_name)
68
+ self.printer = Printer()
69
+ self.client = client or LitellmClient(verbose=self.verbose)
67
70
 
68
- # Initialize role manager
69
- self.role_manager = RoleManager()
71
+ self.bindings = KeyBindings()
70
72
 
71
- # Validate role
72
- if not self.role_manager.role_exists(self.role):
73
- self.console.print(f"Role '{self.role}' not found, using default role.", style="yellow")
74
- self.role = DefaultRoleNames.DEFAULT.value
73
+ self.current_mode: str = TEMP_MODE
75
74
 
76
- # Interactive chat mode settings
77
- self.history = []
78
- self.interactive_max_history = self.config.get("INTERACTIVE_MAX_HISTORY", DEFAULT_INTERACTIVE_ROUND)
79
- self.chat_title = None
75
+ self.interactive_round = cfg["INTERACTIVE_ROUND"]
80
76
  self.chat_start_time = None
81
77
  self.is_temp_session = True
78
+ self.chat = Chat(title="", history=[])
82
79
 
83
80
  # Get and create chat history directory from configuration
84
- self.chat_history_dir = Path(self.config["CHAT_HISTORY_DIR"])
85
- self.chat_history_dir.mkdir(parents=True, exist_ok=True)
86
-
87
- # Initialize chat manager
88
- self.chat_manager = chat_manager or FileChatManager()
81
+ self.chat_history_dir = Path(cfg["CHAT_HISTORY_DIR"])
82
+ # if not self.chat_history_dir.exists():
83
+ # self.chat_history_dir.mkdir(parents=True, exist_ok=True)
89
84
 
90
85
  # Detect OS and Shell if set to auto
91
- if self.config.get("OS_NAME") == DEFAULT_OS_NAME:
92
- self.config["OS_NAME"] = detect_os(self.config)
93
- if self.config.get("SHELL_NAME") == DEFAULT_SHELL_NAME:
94
- self.config["SHELL_NAME"] = detect_shell(self.config)
86
+ if cfg["OS_NAME"] == DEFAULT_OS_NAME:
87
+ cfg["OS_NAME"] = detect_os(cfg)
88
+ if cfg["SHELL_NAME"] == DEFAULT_SHELL_NAME:
89
+ cfg["SHELL_NAME"] = detect_shell(cfg)
95
90
 
96
91
  if self.verbose:
92
+ # Print verbose configuration
97
93
  self.console.print("Loading Configuration:", style="bold cyan")
98
94
  self.console.print(f"Config file path: {CONFIG_PATH}")
99
- for key, value in self.config.items():
95
+ for key, value in cfg.items():
100
96
  display_value = "****" if key == "API_KEY" and value else value
101
- self.console.print(f" {key:<17}: {display_value}")
102
- self.console.print(f"Current role: {self.role}")
103
- self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", DEFAULT_CODE_THEME)))
104
-
105
- self.api_client = api_client or ApiClient(self.config, self.console, self.verbose)
106
- self.printer = printer or Printer(self.config, self.console, self.verbose, markdown=True)
97
+ self.console.print(f" {key:<20}: {display_value}")
98
+ self.console.print(f"Current role: {self.role_name}")
99
+ self.console.print(Markdown("---", code_theme=cfg["CODE_THEME"]))
107
100
 
101
+ # Disable prompt_toolkit warning when use non-tty input,
102
+ # e.g. when use pipe or redirect
108
103
  _origin_stderr = None
109
104
  if not sys.stdin.isatty():
110
105
  _origin_stderr = sys.stderr
@@ -117,46 +112,63 @@ class CLI:
117
112
  sys.stderr.close()
118
113
  sys.stderr = _origin_stderr
119
114
 
120
- def get_prompt_tokens(self) -> List[Tuple[str, str]]:
115
+ def set_role(self, role_name: str) -> None:
116
+ self.role_name = role_name
117
+ self.role = self.role_manager.get_role(role_name)
118
+ if role_name in (DefaultRoleNames.CODER, DefaultRoleNames.SHELL):
119
+ cfg["ENABLE_FUNCTIONS"] = False
120
+ if role_name == DefaultRoleNames.CODER:
121
+ self.printer = Printer(content_markdown=False)
122
+ elif role_name == DefaultRoleNames.SHELL:
123
+ self.current_mode = EXEC_MODE
124
+
125
+ @classmethod
126
+ def evaluate_role_name(cls, code: bool = False, shell: bool = False, role: str = ""):
127
+ """
128
+ Judge the role based on the code, shell, and role options.
129
+ Code and shell are highest priority, then role, then default.
130
+ """
131
+ if code is True:
132
+ return DefaultRoleNames.CODER
133
+ if shell is True:
134
+ return DefaultRoleNames.SHELL
135
+ if role:
136
+ return role
137
+ return DefaultRoleNames.DEFAULT
138
+
139
+ def get_prompt_tokens(self) -> list[tuple[str, str]]:
121
140
  """Return prompt tokens for current mode"""
122
141
  mode_icon = "💬" if self.current_mode == CHAT_MODE else "🚀" if self.current_mode == EXEC_MODE else "📝"
123
142
  return [("class:qmark", f" {mode_icon} "), ("class:prompt", "> ")]
124
143
 
125
144
  def _check_history_len(self) -> None:
126
145
  """Check history length and remove the oldest messages if necessary"""
127
- target_len = self.interactive_max_history * 2
128
- if len(self.history) > target_len:
129
- self.history = self.history[-target_len:]
146
+ target_len = self.interactive_round * 2
147
+ if len(self.chat.history) > target_len:
148
+ self.chat.history = self.chat.history[-target_len:]
130
149
  if self.verbose:
131
- self.console.print(f"History trimmed to {target_len} messages.", style="dim")
132
-
133
- # ------------------- Role Command Methods -------------------
134
- def set_role(self, role: str) -> None:
135
- """Set the current role for the assistant"""
136
- if not self.role_manager.role_exists(role):
137
- self.console.print(f"Role '{role}' not found.", style="bold red")
138
- return
139
-
140
- self.role = role
141
- if self.role == DefaultRoleNames.CODER:
142
- self.printer = Printer(self.config, self.console, self.verbose, content_markdown=False)
150
+ self.console.print(f"Dialogue trimmed to {target_len} messages.", style="dim")
143
151
 
144
152
  # ------------------- Chat Command Methods -------------------
145
- def _save_chat(self, title: Optional[str] = None) -> None:
153
+ def _save_chat(self, title: Union[str, None] = None) -> None:
146
154
  """Save current chat history to a file using session manager."""
147
- saved_title = self.chat_manager.save_chat(self.history, title)
155
+ # Update title if provided
156
+ if title:
157
+ self.chat.title = title
148
158
 
149
- if not saved_title:
150
- self.console.print("Failed to save chat.", style="bold red")
159
+ # Save chat and get the saved title back
160
+ try:
161
+ saved_title = self.chat_manager.save_chat(self.chat)
162
+ except ChatSaveError as e:
163
+ self.console.print(f"Failed to save chat: {e}", style="red")
151
164
  return
152
165
 
153
166
  # Session list will be refreshed automatically by the save method
154
167
  self.console.print(f"Chat saved as: {saved_title}", style="bold green")
155
168
 
156
- # If this was a temporary session, mark it as non-temporary now that it's saved
169
+ # Mark session as persistent if it was temporary
157
170
  if self.is_temp_session:
158
171
  self.is_temp_session = False
159
- self.chat_title = saved_title
160
172
  self.chat_start_time = int(time.time())
161
173
  self.console.print(
162
174
  "Session is now marked as persistent and will be auto-saved on exit.", style="bold green"
@@ -164,7 +176,7 @@ class CLI:
164
176
 
165
177
  def _list_chats(self) -> None:
166
178
  """List all saved chat sessions using session manager."""
167
- chats: list[ChatFileInfo] = self.chat_manager.list_chats()
179
+ chats: list[Chat] = self.chat_manager.list_chats()
168
180
 
169
181
  if not chats:
170
182
  self.console.print("No saved chats found.", style="yellow")
@@ -172,9 +184,9 @@ class CLI:
172
184
 
173
185
  self.console.print("Saved Chats:", style="bold underline")
174
186
  for chat in chats:
175
- index = chat["index"]
176
- title = chat["title"]
177
- date = chat.get("date", "")
187
+ index = chat.idx
188
+ title = chat.title
189
+ date = chat.date
178
190
 
179
191
  if date:
180
192
  self.console.print(f"[dim]{index}.[/dim] [bold blue]{title}[/bold blue] - {date}")
@@ -185,7 +197,7 @@ class CLI:
185
197
  """Force refresh the chat list."""
186
198
  self.chat_manager.refresh_chats()
187
199
 
188
- def _load_chat_by_index(self, index: int) -> bool:
200
+ def _load_chat_by_index(self, index: str) -> bool:
189
201
  """Load a chat session by its index using session manager."""
190
202
  if not self.chat_manager.validate_chat_index(index):
191
203
  self.console.print("Invalid chat index.", style="bold red")
@@ -197,15 +209,14 @@ class CLI:
197
209
  self.console.print("Invalid chat index or chat not found.", style="bold red")
198
210
  return False
199
211
 
200
- self.history = chat_data.get("history", [])
201
- self.chat_title = chat_data.get("title")
202
- self.chat_start_time = chat_data.get("timestamp", int(time.time()))
212
+ self.chat = chat_data
213
+ self.chat_start_time = chat_data.date
203
214
  self.is_temp_session = False
204
215
 
205
- self.console.print(f"Loaded chat: {self.chat_title}", style="bold green")
216
+ self.console.print(f"Loaded chat: {self.chat.title}", style="bold green")
206
217
  return True
207
218
 
208
- def _delete_chat_by_index(self, index: int) -> bool:
219
+ def _delete_chat_by_index(self, index: str) -> bool:
209
220
  """Delete a chat session by its index using session manager."""
210
221
  if not self.chat_manager.validate_chat_index(index):
211
222
  self.console.print("Invalid chat index.", style="bold red")
@@ -217,34 +228,41 @@ class CLI:
217
228
  self.console.print("Invalid chat index or chat not found.", style="bold red")
218
229
  return False
219
230
 
220
- if self.chat_manager.delete_chat(index):
221
- self.console.print(f"Deleted chat: {chat_data['title']}", style="bold green")
231
+ if chat_data.path is None:
232
+ self.console.print(f"Chat has no associated file to delete: {chat_data.title}", style="bold red")
233
+ return False
234
+
235
+ if self.chat_manager.delete_chat(chat_data.path):
236
+ self.console.print(f"Deleted chat: {chat_data.title}", style="bold green")
222
237
  return True
223
238
  else:
224
- self.console.print(f"Failed to delete chat: {chat_data['title']}", style="bold red")
239
+ self.console.print(f"Failed to delete chat: {chat_data.title}", style="bold red")
225
240
  return False
226
241
 
227
242
  # ------------------- Special commands -------------------
228
- def _handle_special_commands(self, user_input: str) -> Optional[bool]:
229
- """Handle special command return: True-continue loop, False-exit loop, None-non-special command"""
243
+ def _handle_special_commands(self, user_input: str) -> Union[bool, str]:
244
+ """Handle special command return: True-continue loop, False-exit loop, str-non-special command"""
230
245
  command = user_input.lower().strip()
246
+ if command in CMD_HELP:
247
+ self.print_help()
248
+ return True
231
249
  if command == CMD_EXIT:
232
250
  return False
233
251
  if command == CMD_CLEAR and self.current_mode == CHAT_MODE:
234
- self.history.clear()
252
+ self.chat.history.clear()
235
253
  self.console.print("Chat history cleared", style="bold yellow")
236
254
  return True
237
255
  if command == CMD_HISTORY:
238
- if not self.history:
256
+ if not self.chat.history:
239
257
  self.console.print("History is empty.", style="yellow")
240
258
  else:
241
259
  self.console.print("Chat History:", style="bold underline")
242
- for i in range(0, len(self.history), 2):
243
- user_msg = self.history[i]
244
- assistant_msg = self.history[i + 1] if (i + 1) < len(self.history) else None
245
- self.console.print(f"[dim]{i // 2 + 1}[/dim] [bold blue]User:[/bold blue] {user_msg['content']}")
260
+ for i in range(0, len(self.chat.history), 2):
261
+ user_msg = self.chat.history[i]
262
+ assistant_msg = self.chat.history[i + 1] if (i + 1) < len(self.chat.history) else None
263
+ self.console.print(f"[dim]{i // 2 + 1}[/dim] [bold blue]User:[/bold blue] {user_msg.content}")
246
264
  if assistant_msg:
247
- md = Markdown(assistant_msg["content"], code_theme=self.config["CODE_THEME"])
265
+ md = Markdown(assistant_msg.content, code_theme=cfg["CODE_THEME"])
248
266
  padded_md = Padding(md, (0, 0, 0, 4))
249
267
  self.console.print(" Assistant:", style="bold green")
250
268
  self.console.print(padded_md)
@@ -253,7 +271,7 @@ class CLI:
253
271
  # Handle /save command - optional title parameter
254
272
  if command.startswith(CMD_SAVE_CHAT):
255
273
  parts = command.split(maxsplit=1)
256
- title = parts[1] if len(parts) > 1 else self.chat_title
274
+ title = parts[1] if len(parts) > 1 else self.chat.title
257
275
  self._save_chat(title)
258
276
  return True
259
277
 
@@ -262,8 +280,7 @@ class CLI:
262
280
  parts = command.split(maxsplit=1)
263
281
  if len(parts) == 2 and parts[1].isdigit():
264
282
  # Try to parse as an index first
265
- index = int(parts[1])
266
- self._load_chat_by_index(index=index)
283
+ self._load_chat_by_index(index=parts[1])
267
284
  else:
268
285
  self.console.print(f"Usage: {CMD_LOAD_CHAT} <index>", style="yellow")
269
286
  self._list_chats()
@@ -273,8 +290,7 @@ class CLI:
273
290
  if command.startswith(CMD_DELETE_CHAT):
274
291
  parts = command.split(maxsplit=1)
275
292
  if len(parts) == 2 and parts[1].isdigit():
276
- index = int(parts[1])
277
- self._delete_chat_by_index(index=index)
293
+ self._delete_chat_by_index(index=parts[1])
278
294
  else:
279
295
  self.console.print(f"Usage: {CMD_DELETE_CHAT} <index>", style="yellow")
280
296
  self._list_chats()
@@ -292,12 +308,75 @@ class CLI:
292
308
  new_mode = parts[1]
293
309
  if self.current_mode != new_mode:
294
310
  self.current_mode = new_mode
311
+ self.set_role(DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else self.init_role)
295
312
  else:
296
313
  self.console.print(f"Already in {self.current_mode} mode.", style="yellow")
297
314
  else:
298
315
  self.console.print(f"Usage: {CMD_MODE} {CHAT_MODE}|{EXEC_MODE}", style="yellow")
299
316
  return True
300
- return None
317
+ return user_input
318
+
319
+ def _build_messages(self, user_input: str) -> list[ChatMessage]:
320
+ """Build message list for LLM API"""
321
+ # Create the message list with system prompt
322
+ messages = [ChatMessage(role="system", content=self.role.prompt)]
323
+
324
+ # Add previous conversation if available
325
+ for msg in self.chat.history:
326
+ messages.append(msg)
327
+
328
+ # Add user input
329
+ messages.append(ChatMessage(role="user", content=user_input))
330
+ return messages
331
+
332
+ def _handle_llm_response(self, user_input: str) -> Optional[str]:
333
+ """Get response from API (streaming or normal) and print it.
334
+ Returns the full content string or None if an error occurred.
335
+
336
+ Args:
337
+ user_input (str): The user's input text.
338
+
339
+ Returns:
340
+ Optional[str]: The assistant's response content or None if an error occurred.
341
+ """
342
+ messages = self._build_messages(user_input)
343
+ if self.verbose:
344
+ self.console.print(messages)
345
+ if self.role != DefaultRoleNames.CODER:
346
+ self.console.print("Assistant:", style="bold green")
347
+ try:
348
+ response = self.client.completion(messages, stream=cfg["STREAM"])
349
+ if cfg["STREAM"]:
350
+ content, _ = self.printer.display_stream(response, messages)
351
+ else:
352
+ content, _ = self.printer.display_normal(response, messages)
353
+
354
+ # Just return the content, message addition is handled in _process_user_input
355
+ return content if content is not None else None
356
+ except Exception as e:
357
+ self.console.print(f"Error processing LLM response: {e}", style="red")
358
+ if self.verbose:
359
+ traceback.print_exc()
360
+ return None
361
+
362
+ def _process_user_input(self, user_input: str) -> bool:
363
+ """Process user input: get response, print, update history, maybe execute.
364
+ Returns True to continue REPL, False to exit on critical error.
365
+ """
366
+ content = self._handle_llm_response(user_input)
367
+
368
+ if content is None:
369
+ return True
370
+
371
+ # Update chat history using Chat's add_message method
372
+ self.chat.add_message("user", user_input)
373
+ self.chat.add_message("assistant", content)
374
+
375
+ self._check_history_len()
376
+
377
+ if self.current_mode == EXEC_MODE:
378
+ self._confirm_and_execute(content)
379
+ return True
301
380
 
302
381
  def _confirm_and_execute(self, raw_content: str) -> None:
303
382
  """Review, edit and execute the command"""
@@ -311,7 +390,7 @@ class CLI:
311
390
  _input = Prompt.ask(
312
391
  r"Execute command? \[e]dit, \[y]es, \[n]o",
313
392
  choices=["y", "n", "e"],
314
- default="n",
393
+ default="y",
315
394
  case_sensitive=False,
316
395
  show_choices=False,
317
396
  )
@@ -331,85 +410,41 @@ class CLI:
331
410
  self.console.print("\nEdit cancelled.", style="yellow")
332
411
  return
333
412
  if executed_cmd:
334
- self.console.print("--- Executing --- ", style="bold green")
413
+ self.console.print("Executing...", style="bold green")
335
414
  try:
336
415
  subprocess.call(executed_cmd, shell=True)
337
416
  except Exception as e:
338
- self.console.print(f"[red]Failed to execute command: {e}[/red]")
339
- self.console.print("--- Finished ---", style="bold green")
417
+ self.console.print(f"Failed to execute command: {e}", style="red")
340
418
  elif _input != "e":
341
419
  self.console.print("Execution cancelled.", style="yellow")
342
420
 
343
- # ------------------- LLM Methods -------------------
344
- def get_system_prompt(self) -> str:
345
- """Get the system prompt based on current role and mode"""
346
- # Use the role manager to get the system prompt
347
- return self.role_manager.get_system_prompt(self.role)
348
-
349
- def _build_messages(self, user_input: str) -> List[dict]:
350
- """Build message list for LLM API"""
351
- # Create the message list
352
- messages = [{"role": "system", "content": self.get_system_prompt()}]
353
-
354
- # Add previous conversation if available
355
- for msg in self.history:
356
- messages.append(msg)
357
-
358
- # Add user input
359
- messages.append({"role": "user", "content": user_input})
360
- return messages
361
-
362
- def _handle_llm_response(self, user_input: str) -> Optional[str]:
363
- """Get response from API (streaming or normal) and print it.
364
- Returns the full content string or None if an error occurred.
365
-
366
- Args:
367
- user_input (str): The user's input text.
421
+ # ------------------- REPL Methods -------------------
422
+ def prepare_chat_loop(self) -> None:
423
+ """Setup key bindings and history for interactive modes."""
424
+ self.current_mode = CHAT_MODE
425
+ self._setup_key_bindings()
426
+ HISTORY_FILE.touch(exist_ok=True)
368
427
 
369
- Returns:
370
- Optional[str]: The assistant's response content or None if an error occurred.
371
- """
372
- messages = self._build_messages(user_input)
373
- if self.verbose:
374
- self.console.print(messages)
375
- is_code_mode = self.role == DefaultRoleNames.CODER
428
+ # Set up the prompt session with command history
376
429
  try:
377
- if self.config["STREAM"]:
378
- stream_iterator = self.api_client.stream_completion(messages)
379
- content, reasoning = self.printer.display_stream(stream_iterator, not is_code_mode)
380
- else:
381
- content, reasoning = self.api_client.completion(messages)
382
- self.printer.display_normal(content, reasoning, not is_code_mode)
383
-
384
- if content is not None:
385
- # Add only the content (not reasoning) to history
386
- self.history.extend(
387
- [{"role": "user", "content": user_input}, {"role": "assistant", "content": content}]
388
- )
389
- self._check_history_len()
390
- return content
391
- else:
392
- return None
430
+ self.session = PromptSession(
431
+ key_bindings=self.bindings,
432
+ history=LimitedFileHistory(HISTORY_FILE, max_entries=self.interactive_round),
433
+ auto_suggest=AutoSuggestFromHistory() if cfg.get("AUTO_SUGGEST", True) else None,
434
+ enable_history_search=True,
435
+ )
393
436
  except Exception as e:
394
- self.console.print(f"[red]Error processing LLM response: {e}[/red]")
395
- if self.verbose:
396
- traceback.print_exc()
397
- return None
398
-
399
- def _process_user_input(self, user_input: str) -> bool:
400
- """Process user input: get response, print, update history, maybe execute.
401
- Returns True to continue REPL, False to exit on critical error.
402
- """
403
- content = self._handle_llm_response(user_input)
437
+ self.console.print(f"Error initializing prompt session history: {e}", style="red")
438
+ self.session = PromptSession(key_bindings=self.bindings)
404
439
 
405
- if content is None:
406
- return True
440
+ def _setup_key_bindings(self) -> None:
441
+ """Setup keyboard shortcuts (e.g., TAB for mode switching)."""
407
442
 
408
- if self.current_mode == EXEC_MODE:
409
- self._confirm_and_execute(content)
410
- return True
443
+ @self.bindings.add(Keys.ControlI) # TAB
444
+ def _(event: KeyPressEvent) -> None:
445
+ self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
446
+ self.set_role(DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else self.init_role)
411
447
 
412
- # ------------------- REPL Methods -------------------
413
448
  def _print_welcome_message(self) -> None:
414
449
  """Prints the initial welcome banner and instructions."""
415
450
  self.console.print(
@@ -419,7 +454,7 @@ class CLI:
419
454
  ████ ███████ ██ ██ ██ ██
420
455
  ██ ██ ██ ██ ██ ██ ██
421
456
  ██ ██ ██ ██ ██████ ███████ ██
422
- """,
457
+ """,
423
458
  style="bold cyan",
424
459
  )
425
460
  self.console.print("Welcome to YAICLI!", style="bold")
@@ -429,10 +464,14 @@ class CLI:
429
464
  self.console.print("Current: [bold yellow]Temporary Session[/bold yellow] (use /save to make persistent)")
430
465
  else:
431
466
  self.console.print(
432
- f"Current: [bold green]Persistent Session[/bold green]{f': {self.chat_title}' if self.chat_title else ''}"
467
+ f"Current: [bold green]Persistent Session[/bold green]{f': {self.chat.title}' if self.chat.title else ''}"
433
468
  )
469
+ self.print_help()
434
470
 
471
+ def print_help(self):
435
472
  self.console.print("Press [bold yellow]TAB[/bold yellow] to switch mode")
473
+ help_cmd = "|".join(CMD_HELP)
474
+ self.console.print(f"{help_cmd:<19}: Show help message")
436
475
  self.console.print(f"{CMD_CLEAR:<19}: Clear chat history")
437
476
  self.console.print(f"{CMD_HISTORY:<19}: Show chat history")
438
477
  self.console.print(f"{CMD_LIST_CHATS:<19}: List saved chats")
@@ -451,98 +490,59 @@ class CLI:
451
490
  """Run the main Read-Eval-Print Loop (REPL)."""
452
491
  self.prepare_chat_loop()
453
492
  self._print_welcome_message()
493
+
494
+ # Main REPL loop
454
495
  while True:
455
- self.console.print(Markdown("---", code_theme=self.config["CODE_THEME"]))
496
+ self.console.print(Markdown("---", code_theme=cfg["CODE_THEME"]))
456
497
  try:
498
+ # Get user input
457
499
  user_input = self.session.prompt(self.get_prompt_tokens)
458
500
  user_input = user_input.strip()
459
501
  if not user_input:
460
502
  continue
461
- command_result = self._handle_special_commands(user_input)
462
- if command_result is False:
503
+
504
+ # Handle special commands
505
+ _continue = self._handle_special_commands(user_input)
506
+ if _continue is False: # Exit command
463
507
  break
464
- if command_result is True:
508
+ if _continue is True: # Other special command
509
+ continue
510
+
511
+ # Process regular chat input
512
+ try:
513
+ if not self._process_user_input(user_input):
514
+ break
515
+ except KeyboardInterrupt:
516
+ self.console.print("KeyboardInterrupt", style="yellow")
465
517
  continue
466
- if not self._process_user_input(user_input):
467
- break
468
518
  except (KeyboardInterrupt, EOFError):
469
519
  break
470
520
 
471
521
  # Auto-save chat history when exiting if there are messages and not a temporary session
472
- if not self.is_temp_session:
473
- self._save_chat(self.chat_title)
522
+ if not self.is_temp_session and self.chat.history:
523
+ self._save_chat(self.chat.title)
474
524
 
475
525
  self.console.print("\nExiting YAICLI... Goodbye!", style="bold green")
476
526
 
477
- def prepare_chat_loop(self) -> None:
478
- """Setup key bindings and history for interactive modes."""
479
- self._setup_key_bindings()
480
- self.HISTORY_FILE.touch(exist_ok=True)
481
- try:
482
- self.session = PromptSession(
483
- key_bindings=self.bindings,
484
- history=LimitedFileHistory(self.HISTORY_FILE, max_entries=self.interactive_max_history),
485
- auto_suggest=AutoSuggestFromHistory() if self.config.get("AUTO_SUGGEST", True) else None,
486
- enable_history_search=True,
487
- )
488
- except Exception as e:
489
- self.console.print(f"[red]Error initializing prompt session history: {e}[/red]")
490
- self.session = PromptSession(key_bindings=self.bindings)
491
- if self.chat_title:
492
- chat_info = self.chat_manager.load_chat_by_title(self.chat_title)
493
- self.is_temp_session = False
494
- self.history = chat_info.get("history", [])
495
-
496
- def _setup_key_bindings(self) -> None:
497
- """Setup keyboard shortcuts (e.g., TAB for mode switching)."""
498
-
499
- @self.bindings.add(Keys.ControlI) # TAB
500
- def _(event: KeyPressEvent) -> None:
501
- self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
502
- self.role = DefaultRoleNames.SHELL if self.current_mode == EXEC_MODE else DefaultRoleNames.DEFAULT
503
-
504
- def _run_once(self, input: str, shell: bool) -> None:
505
- """Run a single command (non-interactive)."""
506
- self.current_mode = EXEC_MODE if shell else TEMP_MODE
507
- if not self.config.get("API_KEY"):
508
- self.console.print("[bold red]Error:[/bold red] API key not found.")
509
- raise typer.Exit(code=1)
510
-
511
- content = self._handle_llm_response(input)
512
-
513
- if content is None:
514
- raise typer.Exit(code=1)
515
-
516
- if shell:
517
- self._confirm_and_execute(content)
518
-
519
- # ------------------- Main Entry Point -------------------
520
- def run(
521
- self,
522
- chat: bool,
523
- shell: bool,
524
- input: Optional[str],
525
- role: Optional[str | Literal[DefaultRoleNames.DEFAULT]] = None,
526
- ) -> None:
527
- """Run the CLI in the appropriate mode with the selected role."""
528
- self.set_role(role or self.role)
529
-
530
- # Now handle normal operation
531
- if shell:
532
- # Set mode to shell
533
- self.role = DefaultRoleNames.SHELL
534
- if input:
535
- self._run_once(input, shell=True)
536
- else:
537
- self.console.print("No prompt provided for shell mode.", style="yellow")
538
- elif chat:
539
- # Start interactive chat mode
540
- self.current_mode = CHAT_MODE
541
- self.chat_title = input if input else None
542
- self.prepare_chat_loop()
527
+ def _run_once(self, user_input: str, shell: bool = False, code: bool = False) -> None:
528
+ """Handle default mode"""
529
+ self.set_role(self.evaluate_role_name(code, shell, self.init_role))
530
+ self._process_user_input(user_input)
531
+
532
+ def run(self, chat: bool = False, shell: bool = False, code: bool = False, user_input: Optional[str] = None):
533
+ if not user_input and not chat:
534
+ self.console.print("No input provided.", style="bold red")
535
+ raise typer.Abort()
536
+
537
+ if chat:
538
+ # If user provided a title, try to load that chat
539
+ if user_input and isinstance(user_input, str):
540
+ loaded_chat = self.chat_manager.load_chat_by_title(user_input)
541
+ if loaded_chat:
542
+ self.chat = loaded_chat
543
+ self.is_temp_session = False
544
+ # Run the interactive chat REPL
543
545
  self._run_repl()
544
- elif input:
545
- # Run once with the given prompt
546
- self._run_once(input, shell=False)
547
546
  else:
548
- self.console.print("No chat or prompt provided. Exiting.")
547
+ # Run in single-use mode
548
+ self._run_once(user_input or "", shell=shell, code=code)