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