grucli 3.3.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.
grucli/auth.py ADDED
@@ -0,0 +1,115 @@
1
+ import os
2
+ import json
3
+ import webbrowser
4
+ from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from urllib.parse import urlparse, parse_qs
6
+ import threading
7
+ import requests
8
+ from datetime import datetime
9
+ from google.oauth2.credentials import Credentials
10
+ from google_auth_oauthlib.flow import InstalledAppFlow
11
+ from google.auth.transport.requests import Request
12
+
13
+ from . import config
14
+
15
+ OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'
16
+ OAUTH_CLIENT_SECRET = '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'
17
+ OAUTH_SCOPES = [
18
+ 'https://www.googleapis.com/auth/cloud-platform',
19
+ 'https://www.googleapis.com/auth/userinfo.email',
20
+ 'https://www.googleapis.com/auth/userinfo.profile',
21
+ 'openid',
22
+ ]
23
+
24
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
25
+
26
+ def get_oauth_file_path():
27
+ return os.path.join(config.get_config_dir(), 'google_auth.json')
28
+
29
+ def get_oauth_credentials():
30
+ creds = None
31
+ oauth_file = get_oauth_file_path()
32
+ if os.path.exists(oauth_file):
33
+ try:
34
+ with open(oauth_file, 'r') as f:
35
+ creds_data = json.load(f)
36
+
37
+ expiry = None
38
+ if creds_data.get('expiry'):
39
+ expiry_str = creds_data.get('expiry')
40
+ if expiry_str.endswith('Z'):
41
+ expiry_str = expiry_str[:-1] + '+00:00'
42
+ expiry = datetime.fromisoformat(expiry_str)
43
+
44
+ creds = Credentials(
45
+ token=creds_data.get('access_token'),
46
+ refresh_token=creds_data.get('refresh_token'),
47
+ token_uri='https://oauth2.googleapis.com/token',
48
+ client_id=OAUTH_CLIENT_ID,
49
+ client_secret=OAUTH_CLIENT_SECRET,
50
+ scopes=creds_data.get('scope').split() if creds_data.get('scope') else OAUTH_SCOPES,
51
+ expiry=expiry
52
+ )
53
+ except Exception as e:
54
+ print(f"Error loading credentials: {e}")
55
+
56
+ if not creds or not creds.valid:
57
+ if creds and creds.expired and creds.refresh_token:
58
+ try:
59
+ creds.refresh(Request())
60
+ save_oauth_credentials(creds)
61
+ except Exception as e:
62
+ print(f"Error refreshing credentials: {e}")
63
+ creds = None
64
+ else:
65
+ creds = None
66
+
67
+ return creds
68
+
69
+ def save_oauth_credentials(creds):
70
+ oauth_file = get_oauth_file_path()
71
+ os.makedirs(os.path.dirname(oauth_file), exist_ok=True)
72
+
73
+ creds_data = {
74
+ 'access_token': creds.token,
75
+ 'refresh_token': creds.refresh_token,
76
+ 'scope': ' '.join(creds.scopes) if creds.scopes else None,
77
+ 'token_type': 'Bearer',
78
+ 'expiry': creds.expiry.isoformat() if creds.expiry else None
79
+ }
80
+
81
+ with open(oauth_file, 'w') as f:
82
+ json.dump(creds_data, f, indent=2)
83
+
84
+ def perform_oauth_login():
85
+ client_config = {
86
+ "installed": {
87
+ "client_id": OAUTH_CLIENT_ID,
88
+ "client_secret": OAUTH_CLIENT_SECRET,
89
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
90
+ "token_uri": "https://oauth2.googleapis.com/token",
91
+ }
92
+ }
93
+
94
+ flow = InstalledAppFlow.from_client_config(client_config, OAUTH_SCOPES)
95
+
96
+ print("\n\033[94m--- Google OAuth Login ---\033[0m")
97
+ print("1. A browser window should open automatically.")
98
+ print("2. If it doesn't, look for a URL in the console below.")
99
+ print("3. Follow the instructions to authorize the application.")
100
+ print("--------------------------\n")
101
+
102
+ try:
103
+ creds = flow.run_local_server(port=0)
104
+ save_oauth_credentials(creds)
105
+ print("\n\033[92mLogin successful! Credentials saved.\033[0m")
106
+ return creds
107
+ except Exception as e:
108
+ print(f"\n\033[91mOAuth login failed: {e}\033[0m")
109
+ raise e
110
+
111
+ def get_auth_token():
112
+ creds = get_oauth_credentials()
113
+ if creds:
114
+ return creds.token
115
+ return None
grucli/chat_manager.py ADDED
@@ -0,0 +1,190 @@
1
+ """
2
+ Chat management for grucli - handles saving, loading and managing chat histories.
3
+ """
4
+ import os
5
+ import json
6
+ import time
7
+ from datetime import datetime
8
+ from .theme import Colors, Icons, Borders
9
+ from . import interrupt
10
+ from . import ui
11
+
12
+ CHATS_DIR = os.path.expanduser("~/.grucli/chats/")
13
+
14
+ def ensure_chats_dir():
15
+ """Ensure the chats directory exists."""
16
+ if not os.path.exists(CHATS_DIR):
17
+ os.makedirs(CHATS_DIR)
18
+
19
+ def get_chat_path(name):
20
+ """Get the full path for a chat file."""
21
+ # Simple slugify to avoid issues with filenames
22
+ safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).strip()
23
+ return os.path.join(CHATS_DIR, f"{safe_name}.json")
24
+
25
+ def save_chat(name, messages, model, provider):
26
+ """Save the current chat history."""
27
+ ensure_chats_dir()
28
+ path = get_chat_path(name)
29
+
30
+ chat_data = {
31
+ "name": name,
32
+ "messages": messages,
33
+ "model": model,
34
+ "provider": provider,
35
+ "last_opened": datetime.now().isoformat(),
36
+ "version": 1
37
+ }
38
+
39
+ with open(path, 'w', encoding='utf-8') as f:
40
+ json.dump(chat_data, f, indent=2, ensure_ascii=False)
41
+
42
+ return path
43
+
44
+ def load_chat(name):
45
+ """Load a chat history by name."""
46
+ path = get_chat_path(name)
47
+ if not os.path.exists(path):
48
+ return None
49
+
50
+ with open(path, 'r', encoding='utf-8') as f:
51
+ chat_data = json.load(f)
52
+
53
+ # Update last opened time
54
+ chat_data["last_opened"] = datetime.now().isoformat()
55
+ with open(path, 'w', encoding='utf-8') as f:
56
+ json.dump(chat_data, f, indent=2, ensure_ascii=False)
57
+
58
+ return chat_data
59
+
60
+ def list_chats():
61
+ """List all saved chats with metadata."""
62
+ ensure_chats_dir()
63
+ chats = []
64
+ for filename in os.listdir(CHATS_DIR):
65
+ if filename.endswith(".json"):
66
+ try:
67
+ with open(os.path.join(CHATS_DIR, filename), 'r', encoding='utf-8') as f:
68
+ data = json.load(f)
69
+ chats.append({
70
+ "name": data.get("name", filename[:-5]),
71
+ "filename": filename,
72
+ "last_opened": data.get("last_opened", "Unknown"),
73
+ "model": data.get("model", "unknown")
74
+ })
75
+ except Exception:
76
+ continue
77
+
78
+ # Sort by last opened (newest first)
79
+ chats.sort(key=lambda x: x["last_opened"], reverse=True)
80
+ return chats
81
+
82
+ def delete_chat(name):
83
+ """Delete a saved chat."""
84
+ path = get_chat_path(name)
85
+ if os.path.exists(path):
86
+ os.remove(path)
87
+ return True
88
+ return False
89
+
90
+ def rename_chat(old_name, new_name):
91
+ """Rename a saved chat."""
92
+ old_path = get_chat_path(old_name)
93
+ new_path = get_chat_path(new_name)
94
+
95
+ if os.path.exists(old_path):
96
+ if os.path.exists(new_path):
97
+ return False, "A chat with that name already exists."
98
+
99
+ with open(old_path, 'r', encoding='utf-8') as f:
100
+ data = json.load(f)
101
+
102
+ data["name"] = new_name
103
+
104
+ with open(new_path, 'w', encoding='utf-8') as f:
105
+ json.dump(data, f, indent=2, ensure_ascii=False)
106
+
107
+ os.remove(old_path)
108
+ return True, None
109
+ return False, "Original chat not found."
110
+
111
+ def manage_chats_ui():
112
+ """Interactive UI to manage chats."""
113
+ while True:
114
+ chats = list_chats()
115
+ if not chats:
116
+ print(f"\n{Colors.WARNING}No saved chats found.{Colors.RESET}\n")
117
+ return None
118
+
119
+ title = "Manage Saved Chats"
120
+ options = []
121
+ for chat in chats:
122
+ # Format last opened time for display
123
+ try:
124
+ dt = datetime.fromisoformat(chat["last_opened"])
125
+ last_opened_str = dt.strftime("%Y-%m-%d %H:%M")
126
+ except Exception:
127
+ last_opened_str = chat["last_opened"]
128
+
129
+ label = f"{chat['name']} ({Colors.MUTED}{chat['model']}, {last_opened_str}{Colors.RESET})"
130
+ options.append(label)
131
+
132
+ options.append(f"{Colors.MUTED}Back to chat{Colors.RESET}")
133
+
134
+ try:
135
+ result = ui.select_option(options, title)
136
+ if not result:
137
+ return None
138
+
139
+ _, selected_index = result
140
+
141
+ # Check if 'Back to chat' was selected (it's the last option)
142
+ if selected_index == len(chats):
143
+ return None
144
+
145
+ selected_chat = chats[selected_index]
146
+ selected_chat_name = selected_chat['name']
147
+
148
+ # Action menu for the selected chat
149
+ action_title = f"Chat: {selected_chat_name}"
150
+ actions = [
151
+ f"{Colors.SUCCESS}{Icons.CHECK} Load Chat{Colors.RESET}",
152
+ f"{Colors.WARNING} Rename Chat{Colors.RESET}",
153
+ f"{Colors.ERROR}{Icons.CROSS} Delete Chat{Colors.RESET}",
154
+ f"{Colors.MUTED}Cancel{Colors.RESET}"
155
+ ]
156
+
157
+ action_result = ui.select_option(actions, action_title)
158
+ if not action_result:
159
+ continue
160
+
161
+ _, action_index = action_result
162
+
163
+ if action_index == 0: # Load
164
+ return selected_chat_name
165
+ elif action_index == 1: # Rename
166
+ print(f"\n{Colors.MUTED}Enter new name for '{selected_chat_name}':{Colors.RESET}")
167
+ new_name = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}> {Colors.RESET}").strip()
168
+ if new_name:
169
+ success, err = rename_chat(selected_chat_name, new_name)
170
+ if success:
171
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Chat renamed to '{new_name}'.{Colors.RESET}")
172
+ time.sleep(1)
173
+ else:
174
+ print(f"{Colors.ERROR}{Icons.CROSS} {err}{Colors.RESET}")
175
+ time.sleep(2)
176
+ elif action_index == 2: # Delete
177
+ confirm = interrupt.safe_input(f"{Colors.WARNING}Delete chat '{selected_chat_name}'? (y/n): {Colors.RESET}").strip().lower()
178
+ if confirm == 'y':
179
+ if delete_chat(selected_chat_name):
180
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Chat deleted.{Colors.RESET}")
181
+ time.sleep(1)
182
+ else:
183
+ print(f"{Colors.ERROR}{Icons.CROSS} Failed to delete chat.{Colors.RESET}")
184
+ time.sleep(2)
185
+ # action_index == 3 is Cancel, which just loops back
186
+
187
+ except interrupt.BackSignal:
188
+ return None
189
+ except KeyboardInterrupt:
190
+ return None
grucli/commands.py ADDED
@@ -0,0 +1,318 @@
1
+ """
2
+ Command handling for grucli slash commands.
3
+ """
4
+
5
+ from prompt_toolkit.completion import Completer, Completion
6
+ from prompt_toolkit.formatted_text import HTML
7
+ from . import api
8
+ from . import config
9
+ from . import tools
10
+ from . import auth
11
+ from . import interrupt
12
+ from . import chat_manager
13
+ from . import ui
14
+ from .theme import Colors, Icons, Borders
15
+ import os
16
+ import time
17
+
18
+ CMD_LIST = ['/exit', '/clear', '/help', '/model', '/manage-api-keys', '/gemini-login', '/gemini-auth-mode', '/show-reasoning', '/save', '/load', '/manage-chats']
19
+
20
+
21
+ class ChatCompleter(Completer):
22
+ """Completer for slash commands and @file mentions."""
23
+
24
+ def __init__(self):
25
+ self._file_cache = []
26
+ self._last_cache_time = 0
27
+
28
+ def get_completions(self, document, complete_event):
29
+ text_before_cursor = document.text_before_cursor
30
+ if not text_before_cursor:
31
+ return
32
+
33
+ last_space = text_before_cursor.rfind(' ')
34
+ current_word = text_before_cursor[last_space+1:]
35
+
36
+ if text_before_cursor.startswith('/') and ' ' not in text_before_cursor:
37
+ for cmd in CMD_LIST:
38
+ if cmd.startswith(text_before_cursor):
39
+ yield Completion(
40
+ cmd,
41
+ start_position=-len(text_before_cursor),
42
+ display=HTML(f'<style color="#af5fff">{cmd}</style>')
43
+ )
44
+ return
45
+
46
+ if text_before_cursor.startswith('/load ') and ' ' not in text_before_cursor[6:]:
47
+ chat_prefix = text_before_cursor[6:].lower()
48
+ chats = chat_manager.list_chats()
49
+ for chat in chats:
50
+ name = chat['name']
51
+ if name.lower().startswith(chat_prefix):
52
+ yield Completion(
53
+ name,
54
+ start_position=-len(chat_prefix),
55
+ display=HTML(f'<style color="#af5fff">{name}</style>')
56
+ )
57
+ return
58
+
59
+ if '@' in current_word:
60
+ at_idx = current_word.rfind('@')
61
+ mention = current_word[at_idx+1:]
62
+ mention_lower = mention.lower()
63
+
64
+ # Simple cache to avoid walking disk on every character
65
+ now = time.time()
66
+ if now - self._last_cache_time > 5:
67
+ self._file_cache = tools.list_files_recursive()
68
+ self._last_cache_time = now
69
+
70
+ for file_path in self._file_cache:
71
+ if mention_lower in file_path.lower():
72
+ yield Completion(
73
+ file_path,
74
+ start_position=-len(mention),
75
+ display=HTML(f'<style color="#af5fff">@{file_path}</style>')
76
+ )
77
+
78
+
79
+ completer = ChatCompleter()
80
+
81
+
82
+ def manage_api_key(api_type='gemini'):
83
+ """Manage saved API keys."""
84
+ api_name = api_type.capitalize()
85
+
86
+ print(f"\n{Colors.PRIMARY}{Colors.BOLD}{api_name} API Key Management{Colors.RESET}")
87
+ print(f"{Colors.MUTED}{Borders.HORIZONTAL * 30}{Colors.RESET}\n")
88
+
89
+ print(f" {Colors.SUCCESS}1{Colors.RESET}) Change stored API key")
90
+ print(f" {Colors.WARNING}2{Colors.RESET}) Change password")
91
+ print(f" {Colors.ERROR}3{Colors.RESET}) Remove stored API key")
92
+ print(f" {Colors.MUTED}4{Colors.RESET}) Cancel")
93
+ print()
94
+
95
+ choice = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Choose (1-4): {Colors.RESET}").strip()
96
+
97
+ if choice == '1':
98
+ if config.has_saved_api_key(api_type):
99
+ print(f"\n{Colors.MUTED}Enter new {api_name} API key:{Colors.RESET}")
100
+ new_api_key = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}> {Colors.RESET}").strip()
101
+ if new_api_key:
102
+ config.save_encrypted_api_key(new_api_key, api_type)
103
+ print(f"{Colors.SUCCESS}{Icons.CHECK} API key updated.{Colors.RESET}")
104
+ else:
105
+ print(f"{Colors.WARNING}No API key provided.{Colors.RESET}")
106
+ else:
107
+ print(f"{Colors.WARNING}No saved API key found. Use the main interface to save a new API key.{Colors.RESET}")
108
+
109
+ elif choice == '2':
110
+ config.change_encrypted_api_key_password(api_type)
111
+
112
+ elif choice == '3':
113
+ config.remove_saved_api_key(api_type)
114
+
115
+ elif choice == '4':
116
+ print(f"{Colors.MUTED}Cancelled.{Colors.RESET}")
117
+ else:
118
+ print(f"{Colors.ERROR}Invalid option. Please choose 1-4.{Colors.RESET}")
119
+
120
+
121
+ def handle_command(cmd, state):
122
+ """Handle slash commands."""
123
+ c = cmd.lower().strip()
124
+
125
+ if c == '/exit':
126
+ return "exit"
127
+
128
+ elif c == '/clear':
129
+ # Reload custom prompt on clear
130
+ sys_prompt = api.get_system_prompt()
131
+ state['messages'] = [{"role": "system", "content": sys_prompt}]
132
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Chat cleared.{Colors.RESET}\n")
133
+ return "continue"
134
+
135
+ elif c == '/help':
136
+ print(f"\n{Colors.PRIMARY}{Colors.BOLD}Available Commands{Colors.RESET}")
137
+ print(f"{Colors.MUTED}{Borders.HORIZONTAL * 40}{Colors.RESET}\n")
138
+
139
+ commands_help = [
140
+ ('/help', 'Show this help message'),
141
+ ('/exit', 'Exit the application'),
142
+ ('/clear', 'Clear the current chat history'),
143
+ ('/model', 'Switch between different models'),
144
+ ('/manage-api-keys', 'Manage your saved API keys'),
145
+ ('/gemini-login', 'Login with your Google account'),
146
+ ('/gemini-auth-mode', 'Toggle Google Auth vs API Key mode'),
147
+ ('/show-reasoning', 'Toggle showing reasoning tokens [true/false]'),
148
+ ('/save [name]', 'Save current chat history'),
149
+ ('/load [name]', 'Load a saved chat history'),
150
+ ('/manage-chats', 'Manage (load/rename/delete) saved chats'),
151
+ ]
152
+
153
+ for cmd_name, desc in commands_help:
154
+ print(f" {Colors.SECONDARY}{cmd_name:20}{Colors.RESET} {Colors.MUTED}{desc}{Colors.RESET}")
155
+
156
+ print(f"\n{Colors.MUTED}Tip: Use @filename to attach file contents to your message.{Colors.RESET}\n")
157
+ return "continue"
158
+
159
+ elif c == '/model':
160
+ return "select_model"
161
+
162
+ elif c == '/manage-api-keys':
163
+ print(f"\n{Colors.PRIMARY}{Colors.BOLD}Select API to Manage{Colors.RESET}")
164
+ print(f"{Colors.MUTED}{Borders.HORIZONTAL * 25}{Colors.RESET}\n")
165
+
166
+ print(f" {Colors.SUCCESS}1{Colors.RESET}) OpenAI API")
167
+ print(f" {Colors.SUCCESS}2{Colors.RESET}) Anthropic API")
168
+ print(f" {Colors.SUCCESS}3{Colors.RESET}) Gemini API")
169
+ print(f" {Colors.SUCCESS}4{Colors.RESET}) Cerebras API")
170
+ print(f" {Colors.MUTED}5{Colors.RESET}) Cancel")
171
+ print()
172
+
173
+ choice = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Choose (1-5): {Colors.RESET}").strip()
174
+ if choice == '1':
175
+ manage_api_key('openai')
176
+ elif choice == '2':
177
+ manage_api_key('anthropic')
178
+ elif choice == '3':
179
+ manage_api_key('gemini')
180
+ elif choice == '4':
181
+ manage_api_key('cerebras')
182
+ elif choice == '5':
183
+ print(f"{Colors.MUTED}Cancelled.{Colors.RESET}")
184
+ else:
185
+ print(f"{Colors.ERROR}Invalid option.{Colors.RESET}")
186
+
187
+ return "continue"
188
+
189
+ elif c == '/gemini-login':
190
+ print(f"\n{Colors.INFO}Starting Google OAuth login flow...{Colors.RESET}")
191
+ try:
192
+ auth.perform_oauth_login()
193
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Successfully logged in with Google!{Colors.RESET}")
194
+ config.set_use_google_auth(True)
195
+ print(f"{Colors.MUTED}Switched to Google Auth mode.{Colors.RESET}")
196
+ except Exception as e:
197
+ print(f"{Colors.ERROR}{Icons.CROSS} Login failed: {e}{Colors.RESET}")
198
+ return "continue"
199
+
200
+ elif c == '/gemini-auth-mode':
201
+ current_mode = "Google Auth" if config.is_using_google_auth() else "Gemini API Key"
202
+ print(f"\n{Colors.PRIMARY}{Colors.BOLD}Gemini Authentication Mode{Colors.RESET}")
203
+ print(f"{Colors.MUTED}{Borders.HORIZONTAL * 30}{Colors.RESET}")
204
+ print(f"\n{Colors.MUTED}Current mode:{Colors.RESET} {Colors.SECONDARY}{current_mode}{Colors.RESET}")
205
+
206
+ if config.is_using_google_auth():
207
+ project_id = os.environ.get('GOOGLE_CLOUD_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT_ID') or config.get_google_cloud_project()
208
+ print(f"{Colors.MUTED}Project ID:{Colors.RESET} {Colors.SECONDARY}{project_id or 'Not set'}{Colors.RESET}")
209
+
210
+ print(f"\n {Colors.SUCCESS}1{Colors.RESET}) Use Google Auth (Login with Google)")
211
+ print(f" {Colors.SUCCESS}2{Colors.RESET}) Use Gemini API Key")
212
+ print(f" {Colors.WARNING}3{Colors.RESET}) Set Google Cloud Project ID")
213
+ print(f" {Colors.MUTED}4{Colors.RESET}) Cancel")
214
+ print()
215
+
216
+ choice = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Choose (1-4): {Colors.RESET}").strip()
217
+ if choice == '1':
218
+ config.set_use_google_auth(True)
219
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Switched to Google Auth mode.{Colors.RESET}")
220
+ if not auth.get_auth_token():
221
+ print(f"{Colors.MUTED}No active session found. Starting login...{Colors.RESET}")
222
+ auth.perform_oauth_login()
223
+ elif choice == '2':
224
+ config.set_use_google_auth(False)
225
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Switched to Gemini API Key mode.{Colors.RESET}")
226
+ elif choice == '3':
227
+ new_project_id = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}Enter Project ID: {Colors.RESET}").strip()
228
+ if new_project_id:
229
+ config.set_google_cloud_project(new_project_id)
230
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Project ID set to: {new_project_id}{Colors.RESET}")
231
+ elif choice == '4':
232
+ print(f"{Colors.MUTED}Cancelled.{Colors.RESET}")
233
+ else:
234
+ print(f"{Colors.ERROR}Invalid option.{Colors.RESET}")
235
+ return "continue"
236
+
237
+ elif c.startswith('/show-reasoning'):
238
+ parts = c.split()
239
+ if len(parts) > 1:
240
+ val = parts[1]
241
+ if val in ['true', 'on', '1', 'yes']:
242
+ api.SHOW_REASONING = True
243
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Showing reasoning tokens enabled (session only).{Colors.RESET}")
244
+ elif val in ['false', 'off', '0', 'no']:
245
+ api.SHOW_REASONING = False
246
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Showing reasoning tokens disabled.{Colors.RESET}")
247
+ else:
248
+ print(f"{Colors.ERROR}Invalid value: {val}. Use true/false.{Colors.RESET}")
249
+ else:
250
+ # Toggle if no value provided
251
+ api.SHOW_REASONING = not api.SHOW_REASONING
252
+ status = "enabled" if api.SHOW_REASONING else "disabled"
253
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Showing reasoning tokens {status}.{Colors.RESET}")
254
+ return "continue"
255
+
256
+ elif c.startswith('/save'):
257
+ parts = cmd.split(None, 1)
258
+ if len(parts) > 1:
259
+ name = parts[1].strip()
260
+ else:
261
+ print(f"\n{Colors.MUTED}Enter a name for this chat:{Colors.RESET}")
262
+ name = interrupt.safe_input(f"{Colors.INPUT_ACTIVE}> {Colors.RESET}").strip()
263
+
264
+ if not name:
265
+ print(f"{Colors.ERROR}Chat name required.{Colors.RESET}")
266
+ return "continue"
267
+
268
+ path = chat_manager.save_chat(name, state['messages'], state['current_model'], api.CURRENT_API)
269
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Chat saved to {Colors.SECONDARY}{path}{Colors.RESET}\n")
270
+ return "continue"
271
+
272
+ elif c.startswith('/load'):
273
+ parts = cmd.split(None, 1)
274
+ if len(parts) > 1:
275
+ name = parts[1].strip()
276
+ else:
277
+ # If no name provided, list chats and let user pick
278
+ chats = chat_manager.list_chats()
279
+ if not chats:
280
+ print(f"{Colors.WARNING}No saved chats found.{Colors.RESET}")
281
+ return "continue"
282
+
283
+ title = "Select Chat to Load"
284
+ options = [f"{c['name']} ({Colors.MUTED}{c['model']}{Colors.RESET})" for c in chats]
285
+ result = ui.select_option(options, title)
286
+ if not result:
287
+ return "continue"
288
+ _, idx = result
289
+ name = chats[idx]['name']
290
+
291
+ chat_data = chat_manager.load_chat(name)
292
+ if chat_data:
293
+ state['messages'] = chat_data['messages']
294
+
295
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Chat '{name}' loaded.{Colors.RESET}\n")
296
+ os.system('cls' if os.name == 'nt' else 'clear')
297
+ ui.print_messages(state['messages'])
298
+ return "continue"
299
+ else:
300
+ print(f"{Colors.ERROR}Chat '{name}' not found.{Colors.RESET}")
301
+ return "continue"
302
+
303
+ elif c == '/manage-chats':
304
+ chat_to_load = chat_manager.manage_chats_ui()
305
+ if chat_to_load:
306
+ chat_data = chat_manager.load_chat(chat_to_load)
307
+ if chat_data:
308
+ state['messages'] = chat_data['messages']
309
+
310
+ print(f"{Colors.SUCCESS}{Icons.CHECK} Chat '{chat_to_load}' loaded.{Colors.RESET}\n")
311
+ os.system('cls' if os.name == 'nt' else 'clear')
312
+ ui.print_messages(state['messages'])
313
+ return "continue"
314
+
315
+ else:
316
+ print(f"{Colors.ERROR}Unknown command: {cmd}{Colors.RESET}")
317
+ print(f"{Colors.MUTED}Type /help for available commands.{Colors.RESET}")
318
+ return "continue"