gemcli 1.0.2__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.
Potentially problematic release.
This version of gemcli might be problematic. Click here for more details.
- gemcli-1.0.2.dist-info/METADATA +385 -0
- gemcli-1.0.2.dist-info/RECORD +7 -0
- gemcli-1.0.2.dist-info/WHEEL +5 -0
- gemcli-1.0.2.dist-info/entry_points.txt +2 -0
- gemcli-1.0.2.dist-info/licenses/LICENSE +21 -0
- gemcli-1.0.2.dist-info/top_level.txt +1 -0
- gemini_cli.py +2717 -0
gemini_cli.py
ADDED
|
@@ -0,0 +1,2717 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
GemCLI - Beautiful terminal interface for Gemini AI
|
|
4
|
+
Made by 89P13
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import glob
|
|
12
|
+
import re
|
|
13
|
+
import difflib
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
from typing import Optional, Tuple, List
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# Suppress ALL logging before importing anything
|
|
20
|
+
import logging
|
|
21
|
+
import warnings
|
|
22
|
+
warnings.filterwarnings('ignore')
|
|
23
|
+
|
|
24
|
+
# Disable all logging including loguru
|
|
25
|
+
logging.disable(logging.CRITICAL)
|
|
26
|
+
os.environ['LOGURU_LEVEL'] = 'CRITICAL'
|
|
27
|
+
os.environ['LOGURU_DIAGNOSE'] = 'False'
|
|
28
|
+
|
|
29
|
+
# Redirect stderr to suppress loguru output
|
|
30
|
+
import io
|
|
31
|
+
sys.stderr = io.StringIO()
|
|
32
|
+
|
|
33
|
+
# Third-party imports
|
|
34
|
+
try:
|
|
35
|
+
from gemini_webapi import GeminiClient
|
|
36
|
+
from rich.console import Console
|
|
37
|
+
from rich.markdown import Markdown
|
|
38
|
+
from rich import box
|
|
39
|
+
from rich.theme import Theme
|
|
40
|
+
import questionary
|
|
41
|
+
from questionary import Style
|
|
42
|
+
import base64
|
|
43
|
+
import webbrowser
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
print(f"Missing required library: {e}")
|
|
46
|
+
print("\nPlease install: pip install gemini-webapi rich questionary")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
# Theme configuration
|
|
50
|
+
theme_config = {
|
|
51
|
+
'primary_color': '#FFFFFF', # White (default)
|
|
52
|
+
'secondary_color': '#FF6B9D', # Vibrant pink
|
|
53
|
+
'accent_color': '#FFFFFF', # White (default)
|
|
54
|
+
'text_color': 'white',
|
|
55
|
+
'border_color': '#3a3a3a',
|
|
56
|
+
'success_color': '#00FF88',
|
|
57
|
+
'warning_color': '#FFB700'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Initialize Rich console with custom theme for markdown
|
|
61
|
+
rich_theme = Theme({
|
|
62
|
+
"markdown.link": theme_config['accent_color'],
|
|
63
|
+
"markdown.link_url": theme_config['accent_color'],
|
|
64
|
+
"markdown.code": theme_config['accent_color'],
|
|
65
|
+
"markdown.code_block": theme_config['text_color'],
|
|
66
|
+
})
|
|
67
|
+
console = Console(theme=rich_theme)
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# UI Components
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
# Store last code blocks for copy functionality
|
|
74
|
+
last_code_blocks = []
|
|
75
|
+
|
|
76
|
+
# Git workflow preferences
|
|
77
|
+
git_preferences = {
|
|
78
|
+
'enabled': True, # Enable git integration
|
|
79
|
+
'commit_mode': 'on_exit', # 'immediate' or 'on_exit'
|
|
80
|
+
'auto_push': True,
|
|
81
|
+
'ask_branch': True # Ask to create/select branch when entering semi-agent mode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Diff viewer preferences
|
|
85
|
+
diff_preferences = {
|
|
86
|
+
'enabled': True, # Enable diff viewing
|
|
87
|
+
'editor': 'vscode' # 'vscode', 'default', or 'none'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def clear_screen():
|
|
92
|
+
"""Clear terminal screen."""
|
|
93
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def print_banner():
|
|
97
|
+
"""Display beautiful banner."""
|
|
98
|
+
clear_screen()
|
|
99
|
+
|
|
100
|
+
# Banner with gradient effect from white to black (top to bottom)
|
|
101
|
+
banner_lines = [
|
|
102
|
+
" ██████╗ ███████╗███╗ ███╗ ██████╗██╗ ██╗",
|
|
103
|
+
" ██╔════╝ ██╔════╝████╗ ████║ ██╔════╝██║ ██║",
|
|
104
|
+
" ██║ ███╗█████╗ ██╔████╔██║ ██║ ██║ ██║",
|
|
105
|
+
" ██║ ██║██╔══╝ ██║╚██╔╝██║ ██║ ██║ ██║",
|
|
106
|
+
" ╚██████╔╝███████╗██║ ╚═╝ ██║ ╚██████╗███████╗██║",
|
|
107
|
+
" ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝"
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# Gradient colors - vibrant golden to orange/pink (top to bottom)
|
|
111
|
+
gradient_colors = ['#FFD700', '#FFC700', '#FFB700', '#FF8C00', '#FF6B35', '#FF6B9D']
|
|
112
|
+
|
|
113
|
+
console.print()
|
|
114
|
+
for i, line in enumerate(banner_lines):
|
|
115
|
+
console.print(line, style=f"bold {gradient_colors[i]}", justify="left")
|
|
116
|
+
|
|
117
|
+
# Clickable GitHub credit
|
|
118
|
+
console.print(f"\n[link=https://github.com/Aniketh78][bold {theme_config['accent_color']}]Made by 89P13[/][/link]\n", justify="center")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_file_completions(text: str, current_word: str) -> List[str]:
|
|
122
|
+
"""Get file path completions based on current input - restricted to workspace only."""
|
|
123
|
+
try:
|
|
124
|
+
# Get workspace root (current working directory)
|
|
125
|
+
workspace_root = Path.cwd()
|
|
126
|
+
|
|
127
|
+
# Remove the / prefix if present
|
|
128
|
+
path_word = current_word.lstrip('/')
|
|
129
|
+
|
|
130
|
+
# If word starts with / or contains path separators, try to complete
|
|
131
|
+
if current_word.startswith('/') or '/' in current_word or '\\' in current_word:
|
|
132
|
+
# Extract the directory and partial filename
|
|
133
|
+
path_str = path_word.replace('\\', '/')
|
|
134
|
+
|
|
135
|
+
# Convert relative path to absolute within workspace
|
|
136
|
+
search_base = workspace_root
|
|
137
|
+
if path_str.startswith('./'):
|
|
138
|
+
path_str = path_str[2:]
|
|
139
|
+
|
|
140
|
+
if not path_str or path_str.endswith('/'):
|
|
141
|
+
# User typed / alone or a directory path ending with /, show contents
|
|
142
|
+
search_dir = search_base / path_str.rstrip('/') if path_str else search_base
|
|
143
|
+
pattern = '*'
|
|
144
|
+
else:
|
|
145
|
+
# Extract directory and filename pattern
|
|
146
|
+
last_slash = path_str.rfind('/')
|
|
147
|
+
if last_slash != -1:
|
|
148
|
+
dir_part = path_str[:last_slash]
|
|
149
|
+
file_part = path_str[last_slash + 1:]
|
|
150
|
+
search_dir = search_base / dir_part
|
|
151
|
+
pattern = file_part + '*' if file_part else '*'
|
|
152
|
+
else:
|
|
153
|
+
search_dir = search_base
|
|
154
|
+
pattern = path_str + '*' if path_str else '*'
|
|
155
|
+
|
|
156
|
+
# Get matches within workspace only
|
|
157
|
+
matches = []
|
|
158
|
+
try:
|
|
159
|
+
if search_dir.exists() and search_dir.is_dir():
|
|
160
|
+
for item in search_dir.glob(pattern):
|
|
161
|
+
# Only include items within workspace
|
|
162
|
+
try:
|
|
163
|
+
relative = item.relative_to(workspace_root)
|
|
164
|
+
rel_str = str(relative).replace('\\', '/')
|
|
165
|
+
if item.is_dir():
|
|
166
|
+
matches.append('/' + rel_str + '/')
|
|
167
|
+
else:
|
|
168
|
+
matches.append('/' + rel_str)
|
|
169
|
+
except ValueError:
|
|
170
|
+
# Skip items outside workspace
|
|
171
|
+
continue
|
|
172
|
+
except:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# Filter out common ignore patterns
|
|
176
|
+
matches = [m for m in matches if not any(
|
|
177
|
+
ignore in m for ignore in ['__pycache__', '.git', 'venv', 'node_modules', '.pyc']
|
|
178
|
+
)]
|
|
179
|
+
|
|
180
|
+
return sorted(matches)[:20] # Limit to 20 suggestions
|
|
181
|
+
else:
|
|
182
|
+
return []
|
|
183
|
+
except:
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_directory_completions(text: str, current_word: str) -> List[str]:
|
|
188
|
+
"""Get directory path completions (directories only) - restricted to workspace only."""
|
|
189
|
+
try:
|
|
190
|
+
# Get workspace root (current working directory)
|
|
191
|
+
workspace_root = Path.cwd()
|
|
192
|
+
|
|
193
|
+
# Handle both absolute and relative paths
|
|
194
|
+
if current_word.startswith('/'):
|
|
195
|
+
# Workspace-relative path
|
|
196
|
+
path_word = current_word.lstrip('/')
|
|
197
|
+
search_base = workspace_root
|
|
198
|
+
else:
|
|
199
|
+
# Could be absolute Windows path or relative
|
|
200
|
+
if Path(current_word).is_absolute():
|
|
201
|
+
# Absolute path
|
|
202
|
+
search_base = Path(current_word).parent
|
|
203
|
+
path_word = Path(current_word).name
|
|
204
|
+
else:
|
|
205
|
+
# Relative to workspace
|
|
206
|
+
path_word = current_word
|
|
207
|
+
search_base = workspace_root
|
|
208
|
+
|
|
209
|
+
path_str = path_word.replace('\\', '/')
|
|
210
|
+
|
|
211
|
+
if not path_str or path_str.endswith('/'):
|
|
212
|
+
# Show directories in current level
|
|
213
|
+
search_dir = search_base / path_str.rstrip('/') if path_str else search_base
|
|
214
|
+
pattern = '*'
|
|
215
|
+
else:
|
|
216
|
+
# Extract directory and partial name
|
|
217
|
+
last_slash = path_str.rfind('/')
|
|
218
|
+
if last_slash != -1:
|
|
219
|
+
dir_part = path_str[:last_slash]
|
|
220
|
+
name_part = path_str[last_slash + 1:]
|
|
221
|
+
search_dir = search_base / dir_part
|
|
222
|
+
pattern = name_part + '*' if name_part else '*'
|
|
223
|
+
else:
|
|
224
|
+
search_dir = search_base
|
|
225
|
+
pattern = path_str + '*' if path_str else '*'
|
|
226
|
+
|
|
227
|
+
# Get directory matches only
|
|
228
|
+
matches = []
|
|
229
|
+
try:
|
|
230
|
+
if search_dir.exists() and search_dir.is_dir():
|
|
231
|
+
for item in search_dir.glob(pattern):
|
|
232
|
+
if item.is_dir():
|
|
233
|
+
# Use absolute paths for directory picker
|
|
234
|
+
matches.append(str(item) + os.sep)
|
|
235
|
+
except:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
# Filter out common ignore patterns
|
|
239
|
+
matches = [m for m in matches if not any(
|
|
240
|
+
ignore in m for ignore in ['__pycache__', '.git', 'venv', 'node_modules']
|
|
241
|
+
)]
|
|
242
|
+
|
|
243
|
+
return sorted(matches)[:20]
|
|
244
|
+
except:
|
|
245
|
+
return []
|
|
246
|
+
"""Get file path completions based on current input - restricted to workspace only."""
|
|
247
|
+
try:
|
|
248
|
+
# Get workspace root (CliTool directory)
|
|
249
|
+
workspace_root = Path(__file__).parent.absolute()
|
|
250
|
+
|
|
251
|
+
# Remove the / prefix if present
|
|
252
|
+
path_word = current_word.lstrip('/')
|
|
253
|
+
|
|
254
|
+
# If word starts with / or contains path separators, try to complete
|
|
255
|
+
if current_word.startswith('/') or '/' in current_word or '\\' in current_word:
|
|
256
|
+
# Extract the directory and partial filename
|
|
257
|
+
path_str = path_word.replace('\\', '/')
|
|
258
|
+
|
|
259
|
+
# Convert relative path to absolute within workspace
|
|
260
|
+
search_base = workspace_root
|
|
261
|
+
if path_str.startswith('./'):
|
|
262
|
+
path_str = path_str[2:]
|
|
263
|
+
|
|
264
|
+
if not path_str or path_str.endswith('/'):
|
|
265
|
+
# User typed / alone or a directory path ending with /, show contents
|
|
266
|
+
search_dir = search_base / path_str.rstrip('/') if path_str else search_base
|
|
267
|
+
pattern = '*'
|
|
268
|
+
else:
|
|
269
|
+
# Extract directory and filename pattern
|
|
270
|
+
last_slash = path_str.rfind('/')
|
|
271
|
+
if last_slash != -1:
|
|
272
|
+
dir_part = path_str[:last_slash]
|
|
273
|
+
file_part = path_str[last_slash + 1:]
|
|
274
|
+
search_dir = search_base / dir_part
|
|
275
|
+
pattern = file_part + '*' if file_part else '*'
|
|
276
|
+
else:
|
|
277
|
+
search_dir = search_base
|
|
278
|
+
pattern = path_str + '*' if path_str else '*'
|
|
279
|
+
|
|
280
|
+
# Get matches within workspace only
|
|
281
|
+
matches = []
|
|
282
|
+
try:
|
|
283
|
+
if search_dir.exists() and search_dir.is_dir():
|
|
284
|
+
for item in search_dir.glob(pattern):
|
|
285
|
+
# Only include items within workspace
|
|
286
|
+
try:
|
|
287
|
+
relative = item.relative_to(workspace_root)
|
|
288
|
+
rel_str = str(relative).replace('\\', '/')
|
|
289
|
+
if item.is_dir():
|
|
290
|
+
matches.append('/' + rel_str + '/')
|
|
291
|
+
else:
|
|
292
|
+
matches.append('/' + rel_str)
|
|
293
|
+
except ValueError:
|
|
294
|
+
# Skip items outside workspace
|
|
295
|
+
continue
|
|
296
|
+
except:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
# Filter out common ignore patterns
|
|
300
|
+
matches = [m for m in matches if not any(
|
|
301
|
+
ignore in m for ignore in ['__pycache__', '.git', 'venv', 'node_modules', '.pyc']
|
|
302
|
+
)]
|
|
303
|
+
|
|
304
|
+
return sorted(matches)[:20] # Limit to 20 suggestions
|
|
305
|
+
else:
|
|
306
|
+
return []
|
|
307
|
+
except:
|
|
308
|
+
return []
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async def get_text_input_async(prompt: str) -> str:
|
|
312
|
+
"""Get text input with real-time file path highlighting using /."""
|
|
313
|
+
from prompt_toolkit import PromptSession
|
|
314
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
315
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
316
|
+
from prompt_toolkit.formatted_text import HTML, FormattedText
|
|
317
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
318
|
+
from prompt_toolkit.lexers import Lexer
|
|
319
|
+
from prompt_toolkit.document import Document
|
|
320
|
+
import re as regex
|
|
321
|
+
|
|
322
|
+
class FilePathLexer(Lexer):
|
|
323
|
+
"""Lexer that highlights file paths starting with / in accent color."""
|
|
324
|
+
def lex_document(self, document):
|
|
325
|
+
def get_line(lineno):
|
|
326
|
+
line = document.lines[lineno]
|
|
327
|
+
result = []
|
|
328
|
+
|
|
329
|
+
# Find all file path patterns /something
|
|
330
|
+
pattern = regex.compile(r'(/[^\s]+)')
|
|
331
|
+
last_end = 0
|
|
332
|
+
|
|
333
|
+
for match in pattern.finditer(line):
|
|
334
|
+
# Add text before the match in default style
|
|
335
|
+
if match.start() > last_end:
|
|
336
|
+
result.append(('', line[last_end:match.start()]))
|
|
337
|
+
|
|
338
|
+
# Add the file path in accent color with background
|
|
339
|
+
file_path = match.group(1)
|
|
340
|
+
result.append((f'class:filepath', file_path))
|
|
341
|
+
last_end = match.end()
|
|
342
|
+
|
|
343
|
+
# Add remaining text
|
|
344
|
+
if last_end < len(line):
|
|
345
|
+
result.append(('', line[last_end:]))
|
|
346
|
+
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
return get_line
|
|
350
|
+
|
|
351
|
+
class PathCompleter(Completer):
|
|
352
|
+
def get_completions(self, document, complete_event):
|
|
353
|
+
text = document.text
|
|
354
|
+
text_before_cursor = document.text_before_cursor
|
|
355
|
+
|
|
356
|
+
# Find the last / symbol and get the word starting from it
|
|
357
|
+
slash_pos = text_before_cursor.rfind('/')
|
|
358
|
+
if slash_pos != -1:
|
|
359
|
+
word = text_before_cursor[slash_pos:]
|
|
360
|
+
completions = get_file_completions(text, word)
|
|
361
|
+
for completion in completions:
|
|
362
|
+
# Display just the filename/folder with styled display
|
|
363
|
+
display_name = completion.lstrip('/').split('/')[-1] or completion
|
|
364
|
+
if completion.endswith('/'):
|
|
365
|
+
display_name = '📁 ' + display_name
|
|
366
|
+
else:
|
|
367
|
+
display_name = '📄 ' + display_name
|
|
368
|
+
|
|
369
|
+
# White text in dropdown
|
|
370
|
+
yield Completion(
|
|
371
|
+
completion,
|
|
372
|
+
start_position=-len(word),
|
|
373
|
+
display=FormattedText([('#ffffff bold', display_name)])
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
style = PTStyle.from_dict({
|
|
377
|
+
'prompt': f'{theme_config["primary_color"]} bold',
|
|
378
|
+
'': f'{theme_config["text_color"]}',
|
|
379
|
+
'completion-menu': 'bg:#1a1a2e #ffffff',
|
|
380
|
+
'completion-menu.completion': 'bg:#16213e #ffffff',
|
|
381
|
+
'completion-menu.completion.current': f'bg:{theme_config["accent_color"]} #000000 bold',
|
|
382
|
+
'scrollbar.background': 'bg:#444444',
|
|
383
|
+
'scrollbar.button': f'bg:{theme_config["accent_color"]}',
|
|
384
|
+
'filepath': f'fg:{theme_config["accent_color"]} bold bg:#2a2a3a',
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
# Create key bindings
|
|
388
|
+
kb = KeyBindings()
|
|
389
|
+
|
|
390
|
+
@kb.add('tab')
|
|
391
|
+
def select_completion(event):
|
|
392
|
+
"""Use Tab to accept the current highlighted completion."""
|
|
393
|
+
buff = event.app.current_buffer
|
|
394
|
+
if buff.complete_state:
|
|
395
|
+
buff.complete_state = None
|
|
396
|
+
else:
|
|
397
|
+
buff.start_completion(select_first=True)
|
|
398
|
+
|
|
399
|
+
@kb.add('down')
|
|
400
|
+
def next_completion(event):
|
|
401
|
+
"""Navigate down in completion menu."""
|
|
402
|
+
buff = event.app.current_buffer
|
|
403
|
+
if buff.complete_state:
|
|
404
|
+
buff.complete_next()
|
|
405
|
+
|
|
406
|
+
@kb.add('up')
|
|
407
|
+
def prev_completion(event):
|
|
408
|
+
"""Navigate up in completion menu."""
|
|
409
|
+
buff = event.app.current_buffer
|
|
410
|
+
if buff.complete_state:
|
|
411
|
+
buff.complete_previous()
|
|
412
|
+
|
|
413
|
+
@kb.add('enter')
|
|
414
|
+
def handle_enter(event):
|
|
415
|
+
"""Enter: submit the input."""
|
|
416
|
+
buff = event.app.current_buffer
|
|
417
|
+
if buff.complete_state:
|
|
418
|
+
buff.complete_state = None
|
|
419
|
+
buff.validate_and_handle()
|
|
420
|
+
|
|
421
|
+
# Claude-style prompt with diamond and vibrant colors
|
|
422
|
+
console.print()
|
|
423
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ {prompt}[/]")
|
|
424
|
+
|
|
425
|
+
session = PromptSession(
|
|
426
|
+
completer=PathCompleter(),
|
|
427
|
+
lexer=FilePathLexer(),
|
|
428
|
+
complete_while_typing=True,
|
|
429
|
+
style=style,
|
|
430
|
+
key_bindings=kb,
|
|
431
|
+
message=HTML(f'<style fg="{theme_config["accent_color"]}" bg="">❯ </style>'),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
result = await session.prompt_async()
|
|
435
|
+
|
|
436
|
+
return result.strip()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
async def get_directory_input_async(prompt: str) -> str:
|
|
440
|
+
"""Get directory path input with autocomplete for directories only."""
|
|
441
|
+
from prompt_toolkit import PromptSession
|
|
442
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
443
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
444
|
+
from prompt_toolkit.formatted_text import HTML, FormattedText
|
|
445
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
446
|
+
|
|
447
|
+
class DirectoryCompleter(Completer):
|
|
448
|
+
def get_completions(self, document, complete_event):
|
|
449
|
+
text = document.text
|
|
450
|
+
completions = get_directory_completions(text, text)
|
|
451
|
+
for completion in completions:
|
|
452
|
+
# Display directory name with folder icon
|
|
453
|
+
display_name = Path(completion).name or completion
|
|
454
|
+
display_name = '📁 ' + display_name
|
|
455
|
+
|
|
456
|
+
yield Completion(
|
|
457
|
+
completion,
|
|
458
|
+
start_position=-len(text),
|
|
459
|
+
display=FormattedText([('#ffffff bold', display_name)])
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
style = PTStyle.from_dict({
|
|
463
|
+
'prompt': f'{theme_config["primary_color"]} bold',
|
|
464
|
+
'': f'{theme_config["text_color"]}',
|
|
465
|
+
'completion-menu': 'bg:#1a1a2e #ffffff',
|
|
466
|
+
'completion-menu.completion': 'bg:#16213e #ffffff',
|
|
467
|
+
'completion-menu.completion.current': f'bg:{theme_config["accent_color"]} #000000 bold',
|
|
468
|
+
'scrollbar.background': 'bg:#444444',
|
|
469
|
+
'scrollbar.button': f'bg:{theme_config["accent_color"]}',
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
# Create key bindings
|
|
473
|
+
kb = KeyBindings()
|
|
474
|
+
|
|
475
|
+
@kb.add('tab')
|
|
476
|
+
def select_completion(event):
|
|
477
|
+
buff = event.app.current_buffer
|
|
478
|
+
if buff.complete_state:
|
|
479
|
+
buff.complete_state = None
|
|
480
|
+
else:
|
|
481
|
+
buff.start_completion(select_first=True)
|
|
482
|
+
|
|
483
|
+
@kb.add('down')
|
|
484
|
+
def next_completion(event):
|
|
485
|
+
buff = event.app.current_buffer
|
|
486
|
+
if buff.complete_state:
|
|
487
|
+
buff.complete_next()
|
|
488
|
+
|
|
489
|
+
@kb.add('up')
|
|
490
|
+
def prev_completion(event):
|
|
491
|
+
buff = event.app.current_buffer
|
|
492
|
+
if buff.complete_state:
|
|
493
|
+
buff.complete_previous()
|
|
494
|
+
|
|
495
|
+
@kb.add('enter')
|
|
496
|
+
def handle_enter(event):
|
|
497
|
+
buff = event.app.current_buffer
|
|
498
|
+
if buff.complete_state:
|
|
499
|
+
buff.complete_state = None
|
|
500
|
+
buff.validate_and_handle()
|
|
501
|
+
|
|
502
|
+
console.print()
|
|
503
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ {prompt}[/]")
|
|
504
|
+
|
|
505
|
+
session = PromptSession(
|
|
506
|
+
completer=DirectoryCompleter(),
|
|
507
|
+
complete_while_typing=True,
|
|
508
|
+
style=style,
|
|
509
|
+
key_bindings=kb,
|
|
510
|
+
message=HTML(f'<style fg="{theme_config["accent_color"]}" bg="">❯ </style>'),
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
result = await session.prompt_async()
|
|
514
|
+
|
|
515
|
+
return result.strip()
|
|
516
|
+
"""Get text input with real-time file path highlighting using /."""
|
|
517
|
+
from prompt_toolkit import PromptSession
|
|
518
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
519
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
520
|
+
from prompt_toolkit.formatted_text import HTML, FormattedText
|
|
521
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
522
|
+
from prompt_toolkit.lexers import Lexer
|
|
523
|
+
from prompt_toolkit.document import Document
|
|
524
|
+
import re as regex
|
|
525
|
+
|
|
526
|
+
class FilePathLexer(Lexer):
|
|
527
|
+
"""Lexer that highlights file paths starting with / in accent color."""
|
|
528
|
+
def lex_document(self, document):
|
|
529
|
+
def get_line(lineno):
|
|
530
|
+
line = document.lines[lineno]
|
|
531
|
+
result = []
|
|
532
|
+
|
|
533
|
+
# Find all file path patterns /something
|
|
534
|
+
pattern = regex.compile(r'(/[^\s]+)')
|
|
535
|
+
last_end = 0
|
|
536
|
+
|
|
537
|
+
for match in pattern.finditer(line):
|
|
538
|
+
# Add text before the match in default style
|
|
539
|
+
if match.start() > last_end:
|
|
540
|
+
result.append(('', line[last_end:match.start()]))
|
|
541
|
+
|
|
542
|
+
# Add the file path in accent color with background
|
|
543
|
+
file_path = match.group(1)
|
|
544
|
+
result.append((f'class:filepath', file_path))
|
|
545
|
+
last_end = match.end()
|
|
546
|
+
|
|
547
|
+
# Add remaining text
|
|
548
|
+
if last_end < len(line):
|
|
549
|
+
result.append(('', line[last_end:]))
|
|
550
|
+
|
|
551
|
+
return result
|
|
552
|
+
|
|
553
|
+
return get_line
|
|
554
|
+
|
|
555
|
+
class PathCompleter(Completer):
|
|
556
|
+
def get_completions(self, document, complete_event):
|
|
557
|
+
text = document.text
|
|
558
|
+
text_before_cursor = document.text_before_cursor
|
|
559
|
+
|
|
560
|
+
# Find the last / symbol and get the word starting from it
|
|
561
|
+
slash_pos = text_before_cursor.rfind('/')
|
|
562
|
+
if slash_pos != -1:
|
|
563
|
+
word = text_before_cursor[slash_pos:]
|
|
564
|
+
completions = get_file_completions(text, word)
|
|
565
|
+
for completion in completions:
|
|
566
|
+
# Display just the filename/folder with styled display
|
|
567
|
+
display_name = completion.lstrip('/').split('/')[-1] or completion
|
|
568
|
+
if completion.endswith('/'):
|
|
569
|
+
display_name = '📁 ' + display_name
|
|
570
|
+
else:
|
|
571
|
+
display_name = '📄 ' + display_name
|
|
572
|
+
|
|
573
|
+
# White text in dropdown
|
|
574
|
+
yield Completion(
|
|
575
|
+
completion,
|
|
576
|
+
start_position=-len(word),
|
|
577
|
+
display=FormattedText([('#ffffff bold', display_name)])
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
style = PTStyle.from_dict({
|
|
581
|
+
'prompt': f'{theme_config["primary_color"]} bold',
|
|
582
|
+
'': f'{theme_config["text_color"]}',
|
|
583
|
+
'completion-menu': 'bg:#1a1a2e #ffffff',
|
|
584
|
+
'completion-menu.completion': 'bg:#16213e #ffffff',
|
|
585
|
+
'completion-menu.completion.current': f'bg:{theme_config["accent_color"]} #000000 bold',
|
|
586
|
+
'scrollbar.background': 'bg:#444444',
|
|
587
|
+
'scrollbar.button': f'bg:{theme_config["accent_color"]}',
|
|
588
|
+
'filepath': f'fg:{theme_config["accent_color"]} bold bg:#2a2a3a',
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
# Create key bindings
|
|
592
|
+
kb = KeyBindings()
|
|
593
|
+
|
|
594
|
+
@kb.add('tab')
|
|
595
|
+
def select_completion(event):
|
|
596
|
+
"""Use Tab to accept the current highlighted completion."""
|
|
597
|
+
buff = event.app.current_buffer
|
|
598
|
+
if buff.complete_state:
|
|
599
|
+
buff.complete_state = None
|
|
600
|
+
else:
|
|
601
|
+
buff.start_completion(select_first=True)
|
|
602
|
+
|
|
603
|
+
@kb.add('down')
|
|
604
|
+
def next_completion(event):
|
|
605
|
+
"""Navigate down in completion menu."""
|
|
606
|
+
buff = event.app.current_buffer
|
|
607
|
+
if buff.complete_state:
|
|
608
|
+
buff.complete_next()
|
|
609
|
+
|
|
610
|
+
@kb.add('up')
|
|
611
|
+
def prev_completion(event):
|
|
612
|
+
"""Navigate up in completion menu."""
|
|
613
|
+
buff = event.app.current_buffer
|
|
614
|
+
if buff.complete_state:
|
|
615
|
+
buff.complete_previous()
|
|
616
|
+
|
|
617
|
+
@kb.add('enter')
|
|
618
|
+
def handle_enter(event):
|
|
619
|
+
"""Enter: submit the input."""
|
|
620
|
+
buff = event.app.current_buffer
|
|
621
|
+
if buff.complete_state:
|
|
622
|
+
buff.complete_state = None
|
|
623
|
+
buff.validate_and_handle()
|
|
624
|
+
|
|
625
|
+
# Claude-style prompt with diamond and vibrant colors
|
|
626
|
+
console.print()
|
|
627
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ {prompt}[/]")
|
|
628
|
+
|
|
629
|
+
session = PromptSession(
|
|
630
|
+
completer=PathCompleter(),
|
|
631
|
+
lexer=FilePathLexer(),
|
|
632
|
+
complete_while_typing=True,
|
|
633
|
+
style=style,
|
|
634
|
+
key_bindings=kb,
|
|
635
|
+
message=HTML(f'<style fg="{theme_config["accent_color"]}" bg="">❯ </style>'),
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
result = await session.prompt_async()
|
|
639
|
+
|
|
640
|
+
return result.strip()
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def get_text_input(prompt: str, placeholder: str = "") -> str:
|
|
645
|
+
"""Get text input with creative styling."""
|
|
646
|
+
console.print()
|
|
647
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ {prompt}[/]")
|
|
648
|
+
if placeholder:
|
|
649
|
+
console.print(f" [dim italic]{placeholder}[/]")
|
|
650
|
+
console.print(f"[{theme_config['accent_color']}]❯[/] ", end="")
|
|
651
|
+
text = input().strip()
|
|
652
|
+
console.print()
|
|
653
|
+
return text
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def typewriter_print(text: str, delay: float = 0.01):
|
|
657
|
+
"""Print text with typewriter effect character by character."""
|
|
658
|
+
import sys
|
|
659
|
+
import time
|
|
660
|
+
|
|
661
|
+
for char in text:
|
|
662
|
+
sys.stdout.write(char)
|
|
663
|
+
sys.stdout.flush()
|
|
664
|
+
# Faster for spaces and newlines, slower for other characters
|
|
665
|
+
if char in ' \n':
|
|
666
|
+
time.sleep(delay * 0.5)
|
|
667
|
+
else:
|
|
668
|
+
time.sleep(delay)
|
|
669
|
+
print() # Final newline
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
async def typewriter_response(response_text: str):
|
|
673
|
+
"""Display response with typewriter effect, handling markdown properly."""
|
|
674
|
+
import time
|
|
675
|
+
import sys
|
|
676
|
+
|
|
677
|
+
# For markdown content, we'll type it out line by line with rich formatting
|
|
678
|
+
lines = response_text.split('\n')
|
|
679
|
+
|
|
680
|
+
for line in lines:
|
|
681
|
+
# Print each line with a slight delay between characters
|
|
682
|
+
if line.strip():
|
|
683
|
+
# Use console for rich formatting, but add small delay between lines
|
|
684
|
+
console.print(Markdown(line))
|
|
685
|
+
await asyncio.sleep(0.03) # Small delay between lines
|
|
686
|
+
else:
|
|
687
|
+
console.print()
|
|
688
|
+
await asyncio.sleep(0.01)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def render_response_with_code_blocks(response_text: str):
|
|
692
|
+
"""Render response with styled code blocks that have copy functionality."""
|
|
693
|
+
import re
|
|
694
|
+
import pyperclip
|
|
695
|
+
|
|
696
|
+
global last_code_blocks
|
|
697
|
+
last_code_blocks = []
|
|
698
|
+
|
|
699
|
+
# Pattern to match code blocks with optional language
|
|
700
|
+
code_pattern = r'```(\w*)\n(.*?)```'
|
|
701
|
+
|
|
702
|
+
# Split text by code blocks
|
|
703
|
+
parts = re.split(r'(```\w*\n.*?```)', response_text, flags=re.DOTALL)
|
|
704
|
+
|
|
705
|
+
code_index = 0
|
|
706
|
+
for part in parts:
|
|
707
|
+
code_match = re.match(r'```(\w*)\n(.*?)```', part, re.DOTALL)
|
|
708
|
+
if code_match:
|
|
709
|
+
language = code_match.group(1) or 'code'
|
|
710
|
+
code_content = code_match.group(2).rstrip()
|
|
711
|
+
code_index += 1
|
|
712
|
+
|
|
713
|
+
# Store code for copy functionality
|
|
714
|
+
last_code_blocks.append(code_content)
|
|
715
|
+
|
|
716
|
+
# Print styled code block header
|
|
717
|
+
console.print()
|
|
718
|
+
console.print(f"[{theme_config['border_color']}]┌─[/] [{theme_config['accent_color']} bold]{language}[/] [{theme_config['border_color']}]{'─' * 60}[/] [dim][ /copy {code_index} ][/]")
|
|
719
|
+
console.print(f"[{theme_config['border_color']}]│[/]")
|
|
720
|
+
|
|
721
|
+
# Print code with syntax highlighting
|
|
722
|
+
for line in code_content.split('\n'):
|
|
723
|
+
console.print(f"[{theme_config['border_color']}]│[/] [{theme_config['text_color']}]{line}[/]")
|
|
724
|
+
|
|
725
|
+
console.print(f"[{theme_config['border_color']}]│[/]")
|
|
726
|
+
console.print(f"[{theme_config['border_color']}]└{'─' * 70}[/]")
|
|
727
|
+
console.print()
|
|
728
|
+
else:
|
|
729
|
+
# Render regular markdown
|
|
730
|
+
if part.strip():
|
|
731
|
+
console.print(Markdown(part))
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def copy_code_block(index: int) -> bool:
|
|
735
|
+
"""Copy a code block to clipboard by index."""
|
|
736
|
+
try:
|
|
737
|
+
import pyperclip
|
|
738
|
+
if 1 <= index <= len(last_code_blocks):
|
|
739
|
+
pyperclip.copy(last_code_blocks[index - 1])
|
|
740
|
+
return True
|
|
741
|
+
except:
|
|
742
|
+
pass
|
|
743
|
+
return False
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def display_file_diff(file_path: str, old_content: str, new_content: str, is_new: bool = False):
|
|
747
|
+
"""Display a diff between old and new file content with red/green indicators."""
|
|
748
|
+
console.print()
|
|
749
|
+
console.print(f"[bold {theme_config['accent_color']}]━━━ {file_path} {'(NEW FILE)' if is_new else ''} ━━━[/]")
|
|
750
|
+
console.print()
|
|
751
|
+
|
|
752
|
+
if is_new:
|
|
753
|
+
# For new files, show all lines as additions
|
|
754
|
+
lines = new_content.splitlines(keepends=False)
|
|
755
|
+
for line in lines:
|
|
756
|
+
console.print(f"[bold green]+ {line}[/]")
|
|
757
|
+
else:
|
|
758
|
+
# Generate diff for existing files
|
|
759
|
+
old_lines = old_content.splitlines(keepends=False)
|
|
760
|
+
new_lines = new_content.splitlines(keepends=False)
|
|
761
|
+
|
|
762
|
+
diff = difflib.unified_diff(
|
|
763
|
+
old_lines,
|
|
764
|
+
new_lines,
|
|
765
|
+
lineterm='',
|
|
766
|
+
fromfile=f'a/{file_path}',
|
|
767
|
+
tofile=f'b/{file_path}'
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
# Skip the file headers (first 2 lines: --- and +++)
|
|
771
|
+
diff_lines = list(diff)
|
|
772
|
+
for line in diff_lines[2:]:
|
|
773
|
+
if line.startswith('@@'):
|
|
774
|
+
# Hunk header
|
|
775
|
+
console.print(f"[bold cyan]{line}[/]")
|
|
776
|
+
elif line.startswith('+'):
|
|
777
|
+
# Added line
|
|
778
|
+
console.print(f"[bold green]{line}[/]")
|
|
779
|
+
elif line.startswith('-'):
|
|
780
|
+
# Removed line
|
|
781
|
+
console.print(f"[bold red]{line}[/]")
|
|
782
|
+
else:
|
|
783
|
+
# Context line
|
|
784
|
+
console.print(f"[dim]{line}[/]")
|
|
785
|
+
|
|
786
|
+
console.print()
|
|
787
|
+
console.print(f"[bold {theme_config['accent_color']}]━━━ End of {file_path} ━━━[/]")
|
|
788
|
+
console.print()
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def open_vscode_diff(original_path: Path, new_content: str, file_label: str, is_new: bool = False):
|
|
792
|
+
"""Open diff viewer to show changes visually based on user preferences."""
|
|
793
|
+
try:
|
|
794
|
+
# Check if diff viewer is enabled
|
|
795
|
+
if not diff_preferences['enabled']:
|
|
796
|
+
return None
|
|
797
|
+
|
|
798
|
+
# Create a temporary file with the new content
|
|
799
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp', delete=False, encoding='utf-8') as tmp:
|
|
800
|
+
tmp.write(new_content)
|
|
801
|
+
tmp_path = tmp.name
|
|
802
|
+
|
|
803
|
+
# Choose editor based on preferences
|
|
804
|
+
if diff_preferences['editor'] == 'none':
|
|
805
|
+
# Just return tmp path without opening any editor
|
|
806
|
+
return tmp_path
|
|
807
|
+
elif diff_preferences['editor'] == 'default':
|
|
808
|
+
# Use system default editor
|
|
809
|
+
try:
|
|
810
|
+
if os.name == 'nt':
|
|
811
|
+
os.startfile(tmp_path)
|
|
812
|
+
elif sys.platform == 'darwin':
|
|
813
|
+
subprocess.Popen(['open', tmp_path])
|
|
814
|
+
else:
|
|
815
|
+
subprocess.Popen(['xdg-open', tmp_path])
|
|
816
|
+
return tmp_path
|
|
817
|
+
except Exception:
|
|
818
|
+
return tmp_path
|
|
819
|
+
else: # vscode
|
|
820
|
+
# Try multiple ways to launch VS Code on Windows
|
|
821
|
+
vscode_commands = [
|
|
822
|
+
'code',
|
|
823
|
+
'code.cmd',
|
|
824
|
+
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Microsoft VS Code\bin\code.cmd'),
|
|
825
|
+
os.path.expandvars(r'%PROGRAMFILES%\Microsoft VS Code\bin\code.cmd'),
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
launched = False
|
|
829
|
+
for cmd in vscode_commands:
|
|
830
|
+
try:
|
|
831
|
+
if is_new:
|
|
832
|
+
# For new files, just open the temp file in preview mode
|
|
833
|
+
subprocess.Popen([cmd, '--reuse-window', tmp_path],
|
|
834
|
+
stdout=subprocess.DEVNULL,
|
|
835
|
+
stderr=subprocess.DEVNULL,
|
|
836
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
|
|
837
|
+
else:
|
|
838
|
+
# For existing files, open VS Code diff editor
|
|
839
|
+
# Format: code --diff <original> <modified>
|
|
840
|
+
subprocess.Popen([cmd, '--diff', str(original_path), tmp_path],
|
|
841
|
+
stdout=subprocess.DEVNULL,
|
|
842
|
+
stderr=subprocess.DEVNULL,
|
|
843
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
|
|
844
|
+
launched = True
|
|
845
|
+
break
|
|
846
|
+
except (FileNotFoundError, OSError):
|
|
847
|
+
continue
|
|
848
|
+
|
|
849
|
+
if not launched:
|
|
850
|
+
raise FileNotFoundError("VS Code not found. Make sure VS Code is installed and 'code' command is in PATH.")
|
|
851
|
+
|
|
852
|
+
return tmp_path
|
|
853
|
+
except Exception as e:
|
|
854
|
+
console.print(f"[{theme_config['warning_color']}]⚠ Could not open diff viewer: {e}[/]")
|
|
855
|
+
return None
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# =============================================================================
|
|
859
|
+
# Cookie Management
|
|
860
|
+
# =============================================================================
|
|
861
|
+
|
|
862
|
+
def save_cookies(psid: str, psidts: Optional[str] = None):
|
|
863
|
+
"""Save cookies to local file."""
|
|
864
|
+
cookies_file = Path('.gemini_cookies.json')
|
|
865
|
+
cookies_data = {
|
|
866
|
+
'psid': psid,
|
|
867
|
+
'psidts': psidts
|
|
868
|
+
}
|
|
869
|
+
try:
|
|
870
|
+
cookies_file.write_text(json.dumps(cookies_data, indent=2))
|
|
871
|
+
except Exception as e:
|
|
872
|
+
console.print(f"[yellow]⚠ Could not save cookies: {e}[/]")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def load_cookies() -> Tuple[Optional[str], Optional[str]]:
|
|
876
|
+
"""Load cookies from local file."""
|
|
877
|
+
cookies_file = Path('.gemini_cookies.json')
|
|
878
|
+
if cookies_file.exists():
|
|
879
|
+
try:
|
|
880
|
+
cookies_data = json.loads(cookies_file.read_text())
|
|
881
|
+
return cookies_data.get('psid'), cookies_data.get('psidts')
|
|
882
|
+
except Exception as e:
|
|
883
|
+
console.print(f"[yellow]⚠ Could not load cookies: {e}[/]")
|
|
884
|
+
return None, None
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def get_cookies() -> Tuple[Optional[str], Optional[str]]:
|
|
888
|
+
"""Get authentication cookies from user."""
|
|
889
|
+
|
|
890
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Cookie Setup ◆[/]")
|
|
891
|
+
console.rule(style=theme_config['border_color'])
|
|
892
|
+
|
|
893
|
+
console.print(f"\n[{theme_config['text_color']}]◆ To get your authentication cookies:[/]\n")
|
|
894
|
+
console.print(f" [{theme_config['accent_color']}]1.[/] Open [{theme_config['secondary_color']}]https://gemini.google.com[/] in your browser")
|
|
895
|
+
console.print(f" [{theme_config['accent_color']}]2.[/] Press [{theme_config['secondary_color']}]F12[/] to open Developer Tools")
|
|
896
|
+
console.print(f" [{theme_config['accent_color']}]3.[/] Go to: [{theme_config['secondary_color']}]Application[/] → [{theme_config['secondary_color']}]Cookies[/] → [{theme_config['secondary_color']}]https://gemini.google.com[/]")
|
|
897
|
+
console.print(f" [{theme_config['accent_color']}]4.[/] Copy the cookie values below:\n")
|
|
898
|
+
|
|
899
|
+
console.rule(style=theme_config['border_color'])
|
|
900
|
+
|
|
901
|
+
try:
|
|
902
|
+
# Get __Secure-1PSID
|
|
903
|
+
psid = get_text_input("◆ __Secure-1PSID (required)", "Paste cookie value here")
|
|
904
|
+
|
|
905
|
+
if not psid:
|
|
906
|
+
console.print(f"\n[red]✗ No value entered[/]")
|
|
907
|
+
return None, None
|
|
908
|
+
|
|
909
|
+
# Get __Secure-1PSIDTS
|
|
910
|
+
psidts = get_text_input("◆ __Secure-1PSIDTS (optional)", "Press Enter to skip") or None
|
|
911
|
+
|
|
912
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Cookies received successfully[/]\n")
|
|
913
|
+
|
|
914
|
+
return psid, psidts
|
|
915
|
+
|
|
916
|
+
except (KeyboardInterrupt, EOFError):
|
|
917
|
+
console.print(f"\n\n[{theme_config['warning_color']}]⚠ Cancelled[/]")
|
|
918
|
+
return None, None
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
# =============================================================================
|
|
922
|
+
# Gemini Client
|
|
923
|
+
# =============================================================================
|
|
924
|
+
|
|
925
|
+
async def initialize_client(psid: str, psidts: Optional[str] = None) -> Optional[GeminiClient]:
|
|
926
|
+
"""Initialize Gemini client."""
|
|
927
|
+
try:
|
|
928
|
+
with console.status(f"[bold {theme_config['primary_color']}]◆ Gemini is connecting...[/]", spinner="dots"):
|
|
929
|
+
client = GeminiClient(secure_1psid=psid, secure_1psidts=psidts)
|
|
930
|
+
await client.init(timeout=45)
|
|
931
|
+
|
|
932
|
+
console.print(f"[bold {theme_config['success_color']}]✓ Connected to Gemini[/]\n")
|
|
933
|
+
return client
|
|
934
|
+
|
|
935
|
+
except Exception as e:
|
|
936
|
+
console.print(f"[bold red]✗ Failed to connect[/]")
|
|
937
|
+
console.print(f"[red]Error: {e}[/]")
|
|
938
|
+
return None
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
# =============================================================================
|
|
942
|
+
# Workspace Search (for Agent mode)
|
|
943
|
+
# =============================================================================
|
|
944
|
+
|
|
945
|
+
def search_workspace(query: str, file_pattern: str = "**/*", max_results: int = 20) -> List[dict]:
|
|
946
|
+
"""Search workspace for files matching query. Returns metadata only (paths, not content)."""
|
|
947
|
+
workspace_root = Path.cwd()
|
|
948
|
+
results = []
|
|
949
|
+
|
|
950
|
+
try:
|
|
951
|
+
# Search through all files
|
|
952
|
+
for file_path in workspace_root.rglob(file_pattern):
|
|
953
|
+
# Skip directories
|
|
954
|
+
if not file_path.is_file():
|
|
955
|
+
continue
|
|
956
|
+
|
|
957
|
+
# Skip large files and common ignore patterns
|
|
958
|
+
if any(ignore in str(file_path) for ignore in ['__pycache__', '.git', 'venv', 'node_modules', '.pyc']):
|
|
959
|
+
continue
|
|
960
|
+
|
|
961
|
+
try:
|
|
962
|
+
if file_path.stat().st_size > 500_000: # Skip files > 500KB
|
|
963
|
+
continue
|
|
964
|
+
|
|
965
|
+
content = file_path.read_text(errors='ignore')
|
|
966
|
+
|
|
967
|
+
# Check if query matches (case-insensitive, supports regex-like patterns)
|
|
968
|
+
patterns = query.split('|') # Support multiple patterns
|
|
969
|
+
if any(re.search(pattern, content, re.IGNORECASE) for pattern in patterns):
|
|
970
|
+
relative_path = file_path.relative_to(workspace_root)
|
|
971
|
+
results.append({
|
|
972
|
+
'path': str(relative_path).replace('\\', '/'),
|
|
973
|
+
'size': f"{file_path.stat().st_size / 1024:.1f}kb",
|
|
974
|
+
'lines': content.count('\n') + 1
|
|
975
|
+
})
|
|
976
|
+
except Exception:
|
|
977
|
+
continue
|
|
978
|
+
|
|
979
|
+
# Sort by size (smaller files first - easier to digest)
|
|
980
|
+
results.sort(key=lambda x: float(x['size'].replace('kb', '')))
|
|
981
|
+
return results[:max_results]
|
|
982
|
+
except Exception:
|
|
983
|
+
return []
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def read_workspace_files(file_paths: List[str], max_size_per_file: int = 15000) -> dict:
|
|
987
|
+
"""Read specific files from workspace. Returns dict of {path: content}."""
|
|
988
|
+
workspace_root = Path.cwd()
|
|
989
|
+
file_contents = {}
|
|
990
|
+
|
|
991
|
+
for path_str in file_paths:
|
|
992
|
+
try:
|
|
993
|
+
# Normalize path
|
|
994
|
+
path_str = path_str.lstrip('/')
|
|
995
|
+
file_path = workspace_root / path_str
|
|
996
|
+
|
|
997
|
+
if file_path.exists() and file_path.is_file():
|
|
998
|
+
content = file_path.read_text(errors='ignore')
|
|
999
|
+
# Limit content size to avoid token overflow
|
|
1000
|
+
if len(content) > max_size_per_file:
|
|
1001
|
+
content = content[:max_size_per_file] + f"\n\n... [truncated, file is {len(content)} chars total]"
|
|
1002
|
+
file_contents[path_str] = content
|
|
1003
|
+
else:
|
|
1004
|
+
file_contents[path_str] = f"Error: File not found or not accessible"
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
file_contents[path_str] = f"Error reading file: {e}"
|
|
1007
|
+
|
|
1008
|
+
return file_contents
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
# =============================================================================
|
|
1012
|
+
# System Command Execution (for Agent/Semi-Agent mode)
|
|
1013
|
+
# =============================================================================
|
|
1014
|
+
|
|
1015
|
+
def execute_system_command(command: str) -> Tuple[bool, str]:
|
|
1016
|
+
"""
|
|
1017
|
+
Execute system commands like opening applications, adjusting settings, etc.
|
|
1018
|
+
Returns (success: bool, output: str)
|
|
1019
|
+
"""
|
|
1020
|
+
try:
|
|
1021
|
+
if os.name == 'nt': # Windows
|
|
1022
|
+
# Replace bash-style && with PowerShell-style ; for command chaining
|
|
1023
|
+
command = command.replace(' && ', '; ')
|
|
1024
|
+
|
|
1025
|
+
# Use PowerShell for Windows commands
|
|
1026
|
+
result = subprocess.run(
|
|
1027
|
+
['powershell', '-Command', command],
|
|
1028
|
+
capture_output=True,
|
|
1029
|
+
text=True,
|
|
1030
|
+
timeout=30,
|
|
1031
|
+
creationflags=subprocess.CREATE_NO_WINDOW
|
|
1032
|
+
)
|
|
1033
|
+
else: # Unix/Linux/Mac
|
|
1034
|
+
result = subprocess.run(
|
|
1035
|
+
command,
|
|
1036
|
+
shell=True,
|
|
1037
|
+
capture_output=True,
|
|
1038
|
+
text=True,
|
|
1039
|
+
timeout=30
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
output = result.stdout if result.stdout else result.stderr
|
|
1043
|
+
success = result.returncode == 0
|
|
1044
|
+
|
|
1045
|
+
return success, output.strip() if output else "Command executed"
|
|
1046
|
+
|
|
1047
|
+
except subprocess.TimeoutExpired:
|
|
1048
|
+
return False, "Command timed out after 30 seconds"
|
|
1049
|
+
except Exception as e:
|
|
1050
|
+
return False, f"Error executing command: {str(e)}"
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
# =============================================================================
|
|
1054
|
+
# Git Integration
|
|
1055
|
+
# =============================================================================
|
|
1056
|
+
|
|
1057
|
+
def get_git_status() -> dict:
|
|
1058
|
+
"""Get current git status."""
|
|
1059
|
+
try:
|
|
1060
|
+
workspace_root = Path.cwd()
|
|
1061
|
+
|
|
1062
|
+
# Check if git repo
|
|
1063
|
+
result = subprocess.run(
|
|
1064
|
+
['git', 'rev-parse', '--git-dir'],
|
|
1065
|
+
cwd=workspace_root,
|
|
1066
|
+
capture_output=True,
|
|
1067
|
+
text=True,
|
|
1068
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
if result.returncode != 0:
|
|
1072
|
+
return {'is_repo': False}
|
|
1073
|
+
|
|
1074
|
+
# Get status
|
|
1075
|
+
status_result = subprocess.run(
|
|
1076
|
+
['git', 'status', '--porcelain'],
|
|
1077
|
+
cwd=workspace_root,
|
|
1078
|
+
capture_output=True,
|
|
1079
|
+
text=True,
|
|
1080
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
# Get current branch
|
|
1084
|
+
branch_result = subprocess.run(
|
|
1085
|
+
['git', 'branch', '--show-current'],
|
|
1086
|
+
cwd=workspace_root,
|
|
1087
|
+
capture_output=True,
|
|
1088
|
+
text=True,
|
|
1089
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
return {
|
|
1093
|
+
'is_repo': True,
|
|
1094
|
+
'status': status_result.stdout,
|
|
1095
|
+
'branch': branch_result.stdout.strip(),
|
|
1096
|
+
'has_changes': bool(status_result.stdout.strip())
|
|
1097
|
+
}
|
|
1098
|
+
except Exception as e:
|
|
1099
|
+
return {'is_repo': False, 'error': str(e)}
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
async def generate_commit_message(chat, modified_files: List[str]) -> str:
|
|
1103
|
+
"""Generate a commit message based on modified files."""
|
|
1104
|
+
try:
|
|
1105
|
+
# Get diff for context
|
|
1106
|
+
workspace_root = Path.cwd()
|
|
1107
|
+
diff_result = subprocess.run(
|
|
1108
|
+
['git', 'diff', '--staged'] if modified_files else ['git', 'diff'],
|
|
1109
|
+
cwd=workspace_root,
|
|
1110
|
+
capture_output=True,
|
|
1111
|
+
text=True,
|
|
1112
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
# Get diff text safely
|
|
1116
|
+
diff_text = diff_result.stdout[:3000] if diff_result.stdout else "No diff available"
|
|
1117
|
+
|
|
1118
|
+
# Build file list string
|
|
1119
|
+
files_str = ', '.join(modified_files) if modified_files else "all changes"
|
|
1120
|
+
|
|
1121
|
+
prompt = f"""Generate a concise git commit message for these changes.
|
|
1122
|
+
Files modified: {files_str}
|
|
1123
|
+
|
|
1124
|
+
Diff:
|
|
1125
|
+
{diff_text}
|
|
1126
|
+
|
|
1127
|
+
Return ONLY the commit message (one line summary, then optional detailed description). No explanations."""
|
|
1128
|
+
|
|
1129
|
+
response = await chat.send_message(prompt)
|
|
1130
|
+
return response.text.strip() if response and response.text else "Update files"
|
|
1131
|
+
except Exception as e:
|
|
1132
|
+
# Safe fallback message
|
|
1133
|
+
if modified_files:
|
|
1134
|
+
return f"Update {', '.join(modified_files)}"
|
|
1135
|
+
else:
|
|
1136
|
+
return "Update files"
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def git_commit(message: str, files: List[str] = None) -> tuple:
|
|
1140
|
+
"""Commit changes with message."""
|
|
1141
|
+
try:
|
|
1142
|
+
workspace_root = Path.cwd()
|
|
1143
|
+
|
|
1144
|
+
# Add files
|
|
1145
|
+
if files:
|
|
1146
|
+
for file in files:
|
|
1147
|
+
subprocess.run(
|
|
1148
|
+
['git', 'add', file],
|
|
1149
|
+
cwd=workspace_root,
|
|
1150
|
+
check=True,
|
|
1151
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1152
|
+
)
|
|
1153
|
+
else:
|
|
1154
|
+
subprocess.run(
|
|
1155
|
+
['git', 'add', '-A'],
|
|
1156
|
+
cwd=workspace_root,
|
|
1157
|
+
check=True,
|
|
1158
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
# Commit
|
|
1162
|
+
result = subprocess.run(
|
|
1163
|
+
['git', 'commit', '-m', message],
|
|
1164
|
+
cwd=workspace_root,
|
|
1165
|
+
capture_output=True,
|
|
1166
|
+
text=True,
|
|
1167
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
return True, result.stdout
|
|
1171
|
+
except Exception as e:
|
|
1172
|
+
return False, str(e)
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def git_push() -> tuple:
|
|
1176
|
+
"""Push commits to remote."""
|
|
1177
|
+
try:
|
|
1178
|
+
workspace_root = Path.cwd()
|
|
1179
|
+
|
|
1180
|
+
result = subprocess.run(
|
|
1181
|
+
['git', 'push'],
|
|
1182
|
+
cwd=workspace_root,
|
|
1183
|
+
capture_output=True,
|
|
1184
|
+
text=True,
|
|
1185
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
if result.returncode == 0:
|
|
1189
|
+
return True, result.stdout
|
|
1190
|
+
else:
|
|
1191
|
+
return False, result.stderr
|
|
1192
|
+
except Exception as e:
|
|
1193
|
+
return False, str(e)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
# =============================================================================
|
|
1197
|
+
# Image Settings Management
|
|
1198
|
+
# =============================================================================
|
|
1199
|
+
|
|
1200
|
+
# Global image settings
|
|
1201
|
+
image_settings = {
|
|
1202
|
+
'save_path': Path('gemini_images') # Default path
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
async def configure_image_settings():
|
|
1207
|
+
"""Configure image save settings."""
|
|
1208
|
+
clear_screen()
|
|
1209
|
+
print_banner()
|
|
1210
|
+
|
|
1211
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Image Save Settings ◆[/]")
|
|
1212
|
+
console.rule(style=theme_config['border_color'])
|
|
1213
|
+
|
|
1214
|
+
console.print()
|
|
1215
|
+
console.print(f"[{theme_config['text_color']}]Current save path:[/] [{theme_config['accent_color']}]{image_settings['save_path']}[/]")
|
|
1216
|
+
console.print(f"[dim]Type to see directory suggestions. Press Enter to keep current.[/]")
|
|
1217
|
+
console.print()
|
|
1218
|
+
|
|
1219
|
+
# Get new path from user with directory autocomplete
|
|
1220
|
+
new_path = await get_directory_input_async("Enter save directory path")
|
|
1221
|
+
|
|
1222
|
+
if new_path:
|
|
1223
|
+
# Remove trailing separator if present
|
|
1224
|
+
new_path = new_path.rstrip(os.sep)
|
|
1225
|
+
image_settings['save_path'] = Path(new_path)
|
|
1226
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Save path updated to: {image_settings['save_path']}[/]\n")
|
|
1227
|
+
else:
|
|
1228
|
+
console.print(f"\n[{theme_config['text_color']}]Keeping current path[/]\n")
|
|
1229
|
+
|
|
1230
|
+
await asyncio.sleep(1)
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
# =============================================================================
|
|
1234
|
+
# Git Settings Management
|
|
1235
|
+
# =============================================================================
|
|
1236
|
+
|
|
1237
|
+
async def configure_git_settings():
|
|
1238
|
+
"""Configure Git workflow preferences."""
|
|
1239
|
+
clear_screen()
|
|
1240
|
+
print_banner()
|
|
1241
|
+
|
|
1242
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Git Workflow Settings ◆[/]")
|
|
1243
|
+
console.rule(style=theme_config['border_color'])
|
|
1244
|
+
|
|
1245
|
+
custom_style = Style([
|
|
1246
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1247
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1248
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
1249
|
+
])
|
|
1250
|
+
|
|
1251
|
+
console.print()
|
|
1252
|
+
console.print(f"[bold {theme_config['secondary_color']}]Commit Mode:[/]")
|
|
1253
|
+
console.print(f"[dim]When should changes be committed?[/]\n")
|
|
1254
|
+
|
|
1255
|
+
commit_mode_choice = await questionary.select(
|
|
1256
|
+
"",
|
|
1257
|
+
choices=[
|
|
1258
|
+
'◆ Ask on exit (recommended)',
|
|
1259
|
+
'◆ After every change'
|
|
1260
|
+
],
|
|
1261
|
+
pointer="❯",
|
|
1262
|
+
style=custom_style,
|
|
1263
|
+
qmark="",
|
|
1264
|
+
show_selected=False,
|
|
1265
|
+
use_shortcuts=False,
|
|
1266
|
+
default='◆ Ask on exit (recommended)' if git_preferences['commit_mode'] == 'on_exit' else '◆ After every change'
|
|
1267
|
+
).ask_async()
|
|
1268
|
+
|
|
1269
|
+
if commit_mode_choice == '◆ Ask on exit (recommended)':
|
|
1270
|
+
git_preferences['commit_mode'] = 'on_exit'
|
|
1271
|
+
else:
|
|
1272
|
+
git_preferences['commit_mode'] = 'immediate'
|
|
1273
|
+
|
|
1274
|
+
console.print()
|
|
1275
|
+
console.print(f"[bold {theme_config['secondary_color']}]Auto Push:[/]")
|
|
1276
|
+
console.print(f"[dim]Automatically push after committing?[/]\n")
|
|
1277
|
+
|
|
1278
|
+
auto_push_choice = await questionary.select(
|
|
1279
|
+
"",
|
|
1280
|
+
choices=['◆ Yes', '◆ No'],
|
|
1281
|
+
pointer="❯",
|
|
1282
|
+
style=custom_style,
|
|
1283
|
+
qmark="",
|
|
1284
|
+
show_selected=False,
|
|
1285
|
+
use_shortcuts=False,
|
|
1286
|
+
default='◆ Yes' if git_preferences['auto_push'] else '◆ No'
|
|
1287
|
+
).ask_async()
|
|
1288
|
+
|
|
1289
|
+
git_preferences['auto_push'] = (auto_push_choice == '◆ Yes')
|
|
1290
|
+
|
|
1291
|
+
console.print()
|
|
1292
|
+
console.print(f"[bold {theme_config['secondary_color']}]Branch Selection:[/]")
|
|
1293
|
+
console.print(f"[dim]Ask to create/select branch in Semi-Agent mode?[/]\n")
|
|
1294
|
+
|
|
1295
|
+
branch_choice = await questionary.select(
|
|
1296
|
+
"",
|
|
1297
|
+
choices=['◆ Yes', '◆ No'],
|
|
1298
|
+
pointer="❯",
|
|
1299
|
+
style=custom_style,
|
|
1300
|
+
qmark="",
|
|
1301
|
+
show_selected=False,
|
|
1302
|
+
use_shortcuts=False,
|
|
1303
|
+
default='◆ Yes' if git_preferences['ask_branch'] else '◆ No'
|
|
1304
|
+
).ask_async()
|
|
1305
|
+
|
|
1306
|
+
git_preferences['ask_branch'] = (branch_choice == '◆ Yes')
|
|
1307
|
+
|
|
1308
|
+
console.print()
|
|
1309
|
+
console.print(f"[bold {theme_config['success_color']}]✓ Git settings saved[/]")
|
|
1310
|
+
console.print()
|
|
1311
|
+
console.print(f"[dim]Commit mode: {git_preferences['commit_mode']}[/]")
|
|
1312
|
+
console.print(f"[dim]Auto push: {'Yes' if git_preferences['auto_push'] else 'No'}[/]")
|
|
1313
|
+
console.print(f"[dim]Ask branch: {'Yes' if git_preferences['ask_branch'] else 'No'}[/]\n")
|
|
1314
|
+
|
|
1315
|
+
await asyncio.sleep(2)
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
# =============================================================================
|
|
1319
|
+
# Settings Menu
|
|
1320
|
+
# =============================================================================
|
|
1321
|
+
|
|
1322
|
+
async def settings_menu():
|
|
1323
|
+
"""Main settings menu."""
|
|
1324
|
+
while True:
|
|
1325
|
+
clear_screen()
|
|
1326
|
+
print_banner()
|
|
1327
|
+
|
|
1328
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Settings ◆[/]")
|
|
1329
|
+
console.rule(style=theme_config['border_color'])
|
|
1330
|
+
|
|
1331
|
+
custom_style = Style([
|
|
1332
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1333
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1334
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
1335
|
+
('instruction', 'fg:#555555'),
|
|
1336
|
+
])
|
|
1337
|
+
|
|
1338
|
+
console.print()
|
|
1339
|
+
choice = await questionary.select(
|
|
1340
|
+
"Choose a setting to configure:",
|
|
1341
|
+
choices=["◆ GitHub Integration", "◆ View Diff Settings", "◆ Back to Main Menu"],
|
|
1342
|
+
pointer="❯",
|
|
1343
|
+
style=custom_style,
|
|
1344
|
+
qmark="",
|
|
1345
|
+
instruction="(Use arrow keys)",
|
|
1346
|
+
show_selected=False,
|
|
1347
|
+
use_shortcuts=False
|
|
1348
|
+
).ask_async()
|
|
1349
|
+
|
|
1350
|
+
if choice == "◆ Back to Main Menu":
|
|
1351
|
+
return
|
|
1352
|
+
elif choice == "◆ GitHub Integration":
|
|
1353
|
+
await configure_github_integration()
|
|
1354
|
+
elif choice == "◆ View Diff Settings":
|
|
1355
|
+
await configure_diff_settings()
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
async def configure_github_integration():
|
|
1359
|
+
"""Configure GitHub integration settings."""
|
|
1360
|
+
clear_screen()
|
|
1361
|
+
print_banner()
|
|
1362
|
+
|
|
1363
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ GitHub Integration ◆[/]")
|
|
1364
|
+
console.rule(style=theme_config['border_color'])
|
|
1365
|
+
|
|
1366
|
+
custom_style = Style([
|
|
1367
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1368
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1369
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
1370
|
+
])
|
|
1371
|
+
|
|
1372
|
+
console.print()
|
|
1373
|
+
console.print(f"[bold {theme_config['secondary_color']}]Enable Git Integration:[/]")
|
|
1374
|
+
console.print(f"[dim]Enable automatic git operations (commits, pushes)?[/]\n")
|
|
1375
|
+
|
|
1376
|
+
enable_choice = await questionary.select(
|
|
1377
|
+
"",
|
|
1378
|
+
choices=['◆ Yes', '◆ No'],
|
|
1379
|
+
pointer="❯",
|
|
1380
|
+
style=custom_style,
|
|
1381
|
+
qmark="",
|
|
1382
|
+
show_selected=False,
|
|
1383
|
+
use_shortcuts=False,
|
|
1384
|
+
default='◆ Yes' if git_preferences['enabled'] else '◆ No'
|
|
1385
|
+
).ask_async()
|
|
1386
|
+
|
|
1387
|
+
git_preferences['enabled'] = (enable_choice == '◆ Yes')
|
|
1388
|
+
|
|
1389
|
+
if git_preferences['enabled']:
|
|
1390
|
+
# Ask for commit mode
|
|
1391
|
+
console.print()
|
|
1392
|
+
console.print(f"[bold {theme_config['secondary_color']}]Commit Mode:[/]")
|
|
1393
|
+
console.print(f"[dim]When should changes be committed?[/]\n")
|
|
1394
|
+
|
|
1395
|
+
commit_mode_choice = await questionary.select(
|
|
1396
|
+
"",
|
|
1397
|
+
choices=[
|
|
1398
|
+
'◆ Ask on exit (recommended)',
|
|
1399
|
+
'◆ After every change'
|
|
1400
|
+
],
|
|
1401
|
+
pointer="❯",
|
|
1402
|
+
style=custom_style,
|
|
1403
|
+
qmark="",
|
|
1404
|
+
show_selected=False,
|
|
1405
|
+
use_shortcuts=False,
|
|
1406
|
+
default='◆ Ask on exit (recommended)' if git_preferences['commit_mode'] == 'on_exit' else '◆ After every change'
|
|
1407
|
+
).ask_async()
|
|
1408
|
+
|
|
1409
|
+
if commit_mode_choice == '◆ Ask on exit (recommended)':
|
|
1410
|
+
git_preferences['commit_mode'] = 'on_exit'
|
|
1411
|
+
else:
|
|
1412
|
+
git_preferences['commit_mode'] = 'immediate'
|
|
1413
|
+
|
|
1414
|
+
# Ask for auto push
|
|
1415
|
+
console.print()
|
|
1416
|
+
console.print(f"[bold {theme_config['secondary_color']}]Auto Push:[/]")
|
|
1417
|
+
console.print(f"[dim]Automatically push after committing?[/]\n")
|
|
1418
|
+
|
|
1419
|
+
auto_push_choice = await questionary.select(
|
|
1420
|
+
"",
|
|
1421
|
+
choices=['◆ Yes', '◆ No'],
|
|
1422
|
+
pointer="❯",
|
|
1423
|
+
style=custom_style,
|
|
1424
|
+
qmark="",
|
|
1425
|
+
show_selected=False,
|
|
1426
|
+
use_shortcuts=False,
|
|
1427
|
+
default='◆ Yes' if git_preferences['auto_push'] else '◆ No'
|
|
1428
|
+
).ask_async()
|
|
1429
|
+
|
|
1430
|
+
git_preferences['auto_push'] = (auto_push_choice == '◆ Yes')
|
|
1431
|
+
|
|
1432
|
+
# Ask for branch selection
|
|
1433
|
+
console.print()
|
|
1434
|
+
console.print(f"[bold {theme_config['secondary_color']}]Branch Selection:[/]")
|
|
1435
|
+
console.print(f"[dim]Ask to create/select branch in Semi-Agent mode?[/]\n")
|
|
1436
|
+
|
|
1437
|
+
branch_choice = await questionary.select(
|
|
1438
|
+
"",
|
|
1439
|
+
choices=['◆ Yes', '◆ No'],
|
|
1440
|
+
pointer="❯",
|
|
1441
|
+
style=custom_style,
|
|
1442
|
+
qmark="",
|
|
1443
|
+
show_selected=False,
|
|
1444
|
+
use_shortcuts=False,
|
|
1445
|
+
default='◆ Yes' if git_preferences['ask_branch'] else '◆ No'
|
|
1446
|
+
).ask_async()
|
|
1447
|
+
|
|
1448
|
+
git_preferences['ask_branch'] = (branch_choice == '◆ Yes')
|
|
1449
|
+
|
|
1450
|
+
console.print()
|
|
1451
|
+
console.print(f"[bold {theme_config['success_color']}]✓ GitHub integration settings saved[/]")
|
|
1452
|
+
console.print()
|
|
1453
|
+
console.print(f"[dim]Git integration: {'Enabled' if git_preferences['enabled'] else 'Disabled'}[/]")
|
|
1454
|
+
if git_preferences['enabled']:
|
|
1455
|
+
console.print(f"[dim]Commit mode: {git_preferences['commit_mode']}[/]")
|
|
1456
|
+
console.print(f"[dim]Auto push: {'Yes' if git_preferences['auto_push'] else 'No'}[/]")
|
|
1457
|
+
console.print(f"[dim]Ask branch: {'Yes' if git_preferences['ask_branch'] else 'No'}[/]")
|
|
1458
|
+
console.print()
|
|
1459
|
+
|
|
1460
|
+
await asyncio.sleep(2)
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
async def configure_diff_settings():
|
|
1464
|
+
"""Configure diff viewing settings."""
|
|
1465
|
+
clear_screen()
|
|
1466
|
+
print_banner()
|
|
1467
|
+
|
|
1468
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ View Diff Settings ◆[/]")
|
|
1469
|
+
console.rule(style=theme_config['border_color'])
|
|
1470
|
+
|
|
1471
|
+
custom_style = Style([
|
|
1472
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1473
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1474
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
1475
|
+
])
|
|
1476
|
+
|
|
1477
|
+
console.print()
|
|
1478
|
+
console.print(f"[bold {theme_config['secondary_color']}]Enable Diff Viewer:[/]")
|
|
1479
|
+
console.print(f"[{theme_config['text_color']}]Show file changes in a diff viewer before applying?[/]\n")
|
|
1480
|
+
|
|
1481
|
+
enable_choice = await questionary.select(
|
|
1482
|
+
"",
|
|
1483
|
+
choices=['◆ Yes', '◆ No'],
|
|
1484
|
+
pointer="❯",
|
|
1485
|
+
style=custom_style,
|
|
1486
|
+
qmark="",
|
|
1487
|
+
show_selected=False,
|
|
1488
|
+
use_shortcuts=False,
|
|
1489
|
+
default='◆ Yes' if diff_preferences['enabled'] else '◆ No'
|
|
1490
|
+
).ask_async()
|
|
1491
|
+
|
|
1492
|
+
diff_preferences['enabled'] = (enable_choice == '◆ Yes')
|
|
1493
|
+
|
|
1494
|
+
if diff_preferences['enabled']:
|
|
1495
|
+
console.print()
|
|
1496
|
+
console.print(f"[bold {theme_config['secondary_color']}]Diff Editor:[/]")
|
|
1497
|
+
console.print(f"[{theme_config['text_color']}]Choose your preferred diff viewer:[/]\n")
|
|
1498
|
+
|
|
1499
|
+
editor_choice = await questionary.select(
|
|
1500
|
+
"",
|
|
1501
|
+
choices=['◆ VS Code', '◆ System Default', '◆ None (terminal only)'],
|
|
1502
|
+
pointer="❯",
|
|
1503
|
+
style=custom_style,
|
|
1504
|
+
qmark="",
|
|
1505
|
+
show_selected=False,
|
|
1506
|
+
use_shortcuts=False,
|
|
1507
|
+
default='◆ VS Code' if diff_preferences['editor'] == 'vscode' else ('◆ System Default' if diff_preferences['editor'] == 'default' else '◆ None (terminal only)')
|
|
1508
|
+
).ask_async()
|
|
1509
|
+
|
|
1510
|
+
if editor_choice == '◆ VS Code':
|
|
1511
|
+
diff_preferences['editor'] = 'vscode'
|
|
1512
|
+
elif editor_choice == '◆ System Default':
|
|
1513
|
+
diff_preferences['editor'] = 'default'
|
|
1514
|
+
else:
|
|
1515
|
+
diff_preferences['editor'] = 'none'
|
|
1516
|
+
|
|
1517
|
+
console.print()
|
|
1518
|
+
console.print(f"[bold {theme_config['success_color']}]✓ Diff viewer settings saved[/]")
|
|
1519
|
+
console.print()
|
|
1520
|
+
console.print(f"[dim]Diff viewer: {'Enabled' if diff_preferences['enabled'] else 'Disabled'}[/]")
|
|
1521
|
+
if diff_preferences['enabled']:
|
|
1522
|
+
console.print(f"[dim]Editor: {diff_preferences['editor']}[/]")
|
|
1523
|
+
console.print()
|
|
1524
|
+
|
|
1525
|
+
await asyncio.sleep(2)
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
async def handle_exit_git_commit(chat):
|
|
1529
|
+
"""Handle git commit when exiting semi-agent mode."""
|
|
1530
|
+
git_info = get_git_status()
|
|
1531
|
+
if git_info['is_repo'] and git_info['has_changes']:
|
|
1532
|
+
console.print()
|
|
1533
|
+
console.print(f"[bold {theme_config['warning_color']}]⚠ You have uncommitted changes[/]\n")
|
|
1534
|
+
|
|
1535
|
+
custom_style = Style([
|
|
1536
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1537
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1538
|
+
])
|
|
1539
|
+
|
|
1540
|
+
# Build choices based on auto_push preference
|
|
1541
|
+
choices = []
|
|
1542
|
+
if git_preferences['auto_push']:
|
|
1543
|
+
choices.extend(['◆ Yes, commit and push', '◆ Yes, commit only'])
|
|
1544
|
+
else:
|
|
1545
|
+
choices.append('◆ Yes, commit')
|
|
1546
|
+
choices.append('◆ No, exit without committing')
|
|
1547
|
+
|
|
1548
|
+
commit_choice = await questionary.select(
|
|
1549
|
+
"Would you like to commit before exiting?",
|
|
1550
|
+
choices=choices,
|
|
1551
|
+
pointer="❯",
|
|
1552
|
+
style=custom_style,
|
|
1553
|
+
qmark="",
|
|
1554
|
+
show_selected=False,
|
|
1555
|
+
use_shortcuts=False
|
|
1556
|
+
).ask_async()
|
|
1557
|
+
|
|
1558
|
+
if commit_choice and 'Yes' in commit_choice:
|
|
1559
|
+
console.print()
|
|
1560
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Generating commit message...[/]")
|
|
1561
|
+
commit_msg = await generate_commit_message(chat, [])
|
|
1562
|
+
|
|
1563
|
+
if commit_msg:
|
|
1564
|
+
console.print()
|
|
1565
|
+
console.print(f"[bold {theme_config['secondary_color']}]◆ Suggested commit message:[/]")
|
|
1566
|
+
console.print(f"[{theme_config['accent_color']}]{commit_msg}[/]")
|
|
1567
|
+
console.print()
|
|
1568
|
+
|
|
1569
|
+
confirm = await questionary.select(
|
|
1570
|
+
"Commit with this message?",
|
|
1571
|
+
choices=['◆ Yes, commit', '◆ No, cancel'],
|
|
1572
|
+
pointer="❯",
|
|
1573
|
+
style=custom_style,
|
|
1574
|
+
qmark="",
|
|
1575
|
+
show_selected=False,
|
|
1576
|
+
use_shortcuts=False
|
|
1577
|
+
).ask_async()
|
|
1578
|
+
|
|
1579
|
+
if confirm == '◆ Yes, commit':
|
|
1580
|
+
success, output = git_commit(commit_msg)
|
|
1581
|
+
if success:
|
|
1582
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Committed successfully[/]")
|
|
1583
|
+
console.print(f"[dim]{output}[/]")
|
|
1584
|
+
|
|
1585
|
+
# Push if user selected commit and push
|
|
1586
|
+
if 'push' in commit_choice.lower():
|
|
1587
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Pushing to remote...[/]")
|
|
1588
|
+
push_success, push_output = git_push()
|
|
1589
|
+
if push_success:
|
|
1590
|
+
console.print(f"[bold {theme_config['success_color']}]✓ Pushed successfully[/]")
|
|
1591
|
+
else:
|
|
1592
|
+
console.print(f"[bold red]✗ Push failed:[/] {push_output}")
|
|
1593
|
+
console.print()
|
|
1594
|
+
else:
|
|
1595
|
+
console.print(f"\n[bold red]✗ Commit failed:[/] {output}\n")
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
def get_git_branches():
|
|
1599
|
+
"""Get list of git branches."""
|
|
1600
|
+
try:
|
|
1601
|
+
# Get local branches only
|
|
1602
|
+
result = subprocess.run(
|
|
1603
|
+
['git', 'branch'],
|
|
1604
|
+
capture_output=True,
|
|
1605
|
+
text=True,
|
|
1606
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
if result.returncode == 0:
|
|
1610
|
+
branches = []
|
|
1611
|
+
current_branch = None
|
|
1612
|
+
for line in result.stdout.splitlines():
|
|
1613
|
+
line = line.strip()
|
|
1614
|
+
if line.startswith('*'):
|
|
1615
|
+
# Current branch
|
|
1616
|
+
current_branch = line[2:].strip()
|
|
1617
|
+
branches.append(current_branch)
|
|
1618
|
+
elif line:
|
|
1619
|
+
# Other local branches
|
|
1620
|
+
branches.append(line)
|
|
1621
|
+
return branches, current_branch
|
|
1622
|
+
return [], None
|
|
1623
|
+
except Exception:
|
|
1624
|
+
return [], None
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
async def handle_branch_selection():
|
|
1628
|
+
"""Handle branch creation or selection for Semi-Agent mode."""
|
|
1629
|
+
git_info = get_git_status()
|
|
1630
|
+
|
|
1631
|
+
if not git_info['is_repo']:
|
|
1632
|
+
return None
|
|
1633
|
+
|
|
1634
|
+
if not git_preferences['ask_branch']:
|
|
1635
|
+
return None
|
|
1636
|
+
|
|
1637
|
+
branches, current_branch = get_git_branches()
|
|
1638
|
+
|
|
1639
|
+
if not branches:
|
|
1640
|
+
return None
|
|
1641
|
+
|
|
1642
|
+
console.print()
|
|
1643
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Git Branch Management ◆[/]")
|
|
1644
|
+
console.print(f"[{theme_config['text_color']}]Current branch:[/] [{theme_config['accent_color']}]{current_branch}[/]\n")
|
|
1645
|
+
|
|
1646
|
+
custom_style = Style([
|
|
1647
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1648
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1649
|
+
])
|
|
1650
|
+
|
|
1651
|
+
choice = await questionary.select(
|
|
1652
|
+
"Would you like to work on a different branch?",
|
|
1653
|
+
choices=[
|
|
1654
|
+
'◆ Stay on current branch',
|
|
1655
|
+
'◆ Create new branch',
|
|
1656
|
+
'◆ Switch to existing branch'
|
|
1657
|
+
],
|
|
1658
|
+
pointer="❯",
|
|
1659
|
+
style=custom_style,
|
|
1660
|
+
qmark="",
|
|
1661
|
+
show_selected=False,
|
|
1662
|
+
use_shortcuts=False
|
|
1663
|
+
).ask_async()
|
|
1664
|
+
|
|
1665
|
+
if choice == '◆ Create new branch':
|
|
1666
|
+
console.print()
|
|
1667
|
+
branch_name = await questionary.text(
|
|
1668
|
+
"Enter new branch name:",
|
|
1669
|
+
style=custom_style,
|
|
1670
|
+
qmark=""
|
|
1671
|
+
).ask_async()
|
|
1672
|
+
|
|
1673
|
+
if branch_name:
|
|
1674
|
+
result = subprocess.run(
|
|
1675
|
+
['git', 'checkout', '-b', branch_name],
|
|
1676
|
+
capture_output=True,
|
|
1677
|
+
text=True,
|
|
1678
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
if result.returncode == 0:
|
|
1682
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Created and switched to branch: {branch_name}[/]\n")
|
|
1683
|
+
return branch_name
|
|
1684
|
+
else:
|
|
1685
|
+
console.print(f"\n[bold red]✗ Failed to create branch: {result.stderr}[/]\n")
|
|
1686
|
+
return None
|
|
1687
|
+
|
|
1688
|
+
elif choice == '◆ Switch to existing branch':
|
|
1689
|
+
console.print()
|
|
1690
|
+
branch_choices = [f'◆ {b}' for b in branches if b != current_branch]
|
|
1691
|
+
|
|
1692
|
+
if not branch_choices:
|
|
1693
|
+
console.print(f"[{theme_config['warning_color']}]No other branches available[/]\n")
|
|
1694
|
+
return None
|
|
1695
|
+
|
|
1696
|
+
selected = await questionary.select(
|
|
1697
|
+
"Select branch:",
|
|
1698
|
+
choices=branch_choices,
|
|
1699
|
+
pointer="❯",
|
|
1700
|
+
style=custom_style,
|
|
1701
|
+
qmark="",
|
|
1702
|
+
show_selected=False,
|
|
1703
|
+
use_shortcuts=False
|
|
1704
|
+
).ask_async()
|
|
1705
|
+
|
|
1706
|
+
if selected:
|
|
1707
|
+
branch_name = selected.replace('◆ ', '')
|
|
1708
|
+
result = subprocess.run(
|
|
1709
|
+
['git', 'checkout', branch_name],
|
|
1710
|
+
capture_output=True,
|
|
1711
|
+
text=True,
|
|
1712
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
if result.returncode == 0:
|
|
1716
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Switched to branch: {branch_name}[/]\n")
|
|
1717
|
+
return branch_name
|
|
1718
|
+
else:
|
|
1719
|
+
console.print(f"\n[bold red]✗ Failed to switch branch: {result.stderr}[/]\n")
|
|
1720
|
+
return None
|
|
1721
|
+
|
|
1722
|
+
return None
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
# =============================================================================
|
|
1726
|
+
# Chat Interface
|
|
1727
|
+
# =============================================================================
|
|
1728
|
+
|
|
1729
|
+
async def send_message(chat, message: str) -> tuple:
|
|
1730
|
+
"""Send message to Gemini chat session. Returns (text, images_list)."""
|
|
1731
|
+
try:
|
|
1732
|
+
response = await chat.send_message(message)
|
|
1733
|
+
|
|
1734
|
+
text = response.text if response and response.text else ""
|
|
1735
|
+
images = []
|
|
1736
|
+
|
|
1737
|
+
# Check for images in response
|
|
1738
|
+
if response and hasattr(response, 'images') and response.images:
|
|
1739
|
+
images = response.images
|
|
1740
|
+
|
|
1741
|
+
# If no text but has images, show placeholder
|
|
1742
|
+
if not text and images:
|
|
1743
|
+
text = "_Image generated successfully_"
|
|
1744
|
+
elif not text and not images:
|
|
1745
|
+
text = "[No response]"
|
|
1746
|
+
|
|
1747
|
+
return text, images
|
|
1748
|
+
except asyncio.TimeoutError:
|
|
1749
|
+
return "[red][Timeout - Please try again][/]", []
|
|
1750
|
+
except Exception as e:
|
|
1751
|
+
return f"[red][Error: {str(e)}][/]", []
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
async def chat_loop(client: GeminiClient, mode: str = "ask"):
|
|
1755
|
+
"""Main chat loop."""
|
|
1756
|
+
|
|
1757
|
+
# Start a chat session for continuous conversation
|
|
1758
|
+
chat = client.start_chat()
|
|
1759
|
+
|
|
1760
|
+
# For agent mode or semi-agent mode, handle branch selection first
|
|
1761
|
+
if mode in ["semi-agent", "agent"] and git_preferences['enabled']:
|
|
1762
|
+
await handle_branch_selection()
|
|
1763
|
+
|
|
1764
|
+
# For agent mode, show instructions
|
|
1765
|
+
if mode == "agent":
|
|
1766
|
+
clear_screen()
|
|
1767
|
+
print_banner()
|
|
1768
|
+
|
|
1769
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Agent Mode ◆[/]")
|
|
1770
|
+
console.rule(style=theme_config['border_color'])
|
|
1771
|
+
console.print()
|
|
1772
|
+
console.print(f"[{theme_config['text_color']}]In this mode, Gemini can:[/]")
|
|
1773
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Search your entire workspace for relevant files")
|
|
1774
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Read specific files it needs")
|
|
1775
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Make coordinated changes across multiple files")
|
|
1776
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Work autonomously without you specifying files")
|
|
1777
|
+
console.print()
|
|
1778
|
+
console.print(f"[{theme_config['secondary_color']}]Just describe what you want to accomplish![/]")
|
|
1779
|
+
console.print()
|
|
1780
|
+
console.rule(style=theme_config['border_color'])
|
|
1781
|
+
console.print()
|
|
1782
|
+
|
|
1783
|
+
# For image mode, show menu first
|
|
1784
|
+
if mode == "image":
|
|
1785
|
+
while True:
|
|
1786
|
+
clear_screen()
|
|
1787
|
+
print_banner()
|
|
1788
|
+
|
|
1789
|
+
console.print()
|
|
1790
|
+
console.print(f"[bold {theme_config['accent_color']}]◆ Image Generation Mode[/]")
|
|
1791
|
+
console.print()
|
|
1792
|
+
console.print(f"[{theme_config['text_color']}]Current save path:[/] [{theme_config['accent_color']}]{image_settings['save_path']}[/]")
|
|
1793
|
+
console.print()
|
|
1794
|
+
|
|
1795
|
+
# Create custom style
|
|
1796
|
+
custom_style = Style([
|
|
1797
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1798
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1799
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
1800
|
+
('instruction', 'fg:#555555'),
|
|
1801
|
+
])
|
|
1802
|
+
|
|
1803
|
+
image_choice = await questionary.select(
|
|
1804
|
+
"Choose an option:",
|
|
1805
|
+
choices=["◆ Generate Images", "◆ Save Settings", "◆ Back to Main Menu"],
|
|
1806
|
+
pointer="❯",
|
|
1807
|
+
style=custom_style,
|
|
1808
|
+
qmark="",
|
|
1809
|
+
instruction="(Use arrow keys)",
|
|
1810
|
+
show_selected=False,
|
|
1811
|
+
use_shortcuts=False
|
|
1812
|
+
).ask_async()
|
|
1813
|
+
|
|
1814
|
+
if image_choice == "◆ Back to Main Menu":
|
|
1815
|
+
return
|
|
1816
|
+
elif image_choice == "◆ Save Settings":
|
|
1817
|
+
await configure_image_settings()
|
|
1818
|
+
continue
|
|
1819
|
+
elif image_choice == "◆ Generate Images":
|
|
1820
|
+
break
|
|
1821
|
+
|
|
1822
|
+
# Display mode indicator with diamonds
|
|
1823
|
+
mode_display = {
|
|
1824
|
+
"ask": "◆ Ask Mode",
|
|
1825
|
+
"semi-agent": "◆ Semi-Agent Mode (AI Coding Assistant)",
|
|
1826
|
+
"agent": "◆ Agent Mode (Autonomous)",
|
|
1827
|
+
"image": "◆ Image Generation Mode"
|
|
1828
|
+
}
|
|
1829
|
+
console.print(f"\n[bold {theme_config['accent_color']}]{mode_display.get(mode, '◆ Ask')}[/]")
|
|
1830
|
+
if mode in ["ask", "semi-agent"]:
|
|
1831
|
+
console.print(f"[dim]◆ Commands: /exit /clear /mode /status /commit /push[/]")
|
|
1832
|
+
console.print(f"[dim]◆ File paths: Type [bold {theme_config['accent_color']}]/[/dim][dim] to see workspace files (e.g., [/dim][{theme_config['accent_color']}]/gemini_cli.py[/][dim])[/]\n")
|
|
1833
|
+
if mode == "semi-agent":
|
|
1834
|
+
console.print(f"[dim italic]Semi-Agent Mode: I can read files, suggest modifications, and apply changes to your code.[/]\n")
|
|
1835
|
+
elif mode == "image":
|
|
1836
|
+
console.print(f"[dim]◆ Commands: /exit /clear /mode[/]")
|
|
1837
|
+
console.print(f"[dim]◆ Example: [/dim][{theme_config['text_color']}]Create a beautiful sunset over mountains[/]\n")
|
|
1838
|
+
else:
|
|
1839
|
+
console.print(f"[dim]◆ Commands: /exit /clear /mode[/]\n")
|
|
1840
|
+
|
|
1841
|
+
while True:
|
|
1842
|
+
try:
|
|
1843
|
+
# Get input with file path autocomplete - Claude-style
|
|
1844
|
+
console.print()
|
|
1845
|
+
except KeyboardInterrupt:
|
|
1846
|
+
console.print(f"\n\n[bold {theme_config['primary_color']}]◆ Goodbye! ◆[/]\n")
|
|
1847
|
+
break
|
|
1848
|
+
|
|
1849
|
+
try:
|
|
1850
|
+
user_input = await get_text_input_async(f"You")
|
|
1851
|
+
except (EOFError, KeyboardInterrupt):
|
|
1852
|
+
console.print(f"\n\n[bold {theme_config['primary_color']}]◆ Goodbye! ◆[/]\n")
|
|
1853
|
+
break
|
|
1854
|
+
|
|
1855
|
+
try:
|
|
1856
|
+
if not user_input:
|
|
1857
|
+
continue
|
|
1858
|
+
|
|
1859
|
+
# Handle commands
|
|
1860
|
+
if user_input.lower() in ('/exit', '/quit', '/q'):
|
|
1861
|
+
# Check for uncommitted changes before exiting
|
|
1862
|
+
if mode == "semi-agent" and git_preferences['enabled'] and git_preferences['commit_mode'] == 'on_exit':
|
|
1863
|
+
await handle_exit_git_commit(chat)
|
|
1864
|
+
|
|
1865
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Goodbye! ◆[/]\n")
|
|
1866
|
+
break
|
|
1867
|
+
|
|
1868
|
+
elif user_input.lower() == '/mode':
|
|
1869
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Returning to mode selection... ◆[/]\n")
|
|
1870
|
+
return
|
|
1871
|
+
|
|
1872
|
+
elif user_input.lower() == '/clear':
|
|
1873
|
+
clear_screen()
|
|
1874
|
+
print_banner()
|
|
1875
|
+
continue
|
|
1876
|
+
|
|
1877
|
+
elif user_input.lower() == '/help':
|
|
1878
|
+
console.print()
|
|
1879
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ GemCLI Help ◆[/]")
|
|
1880
|
+
console.rule(style=theme_config['border_color'])
|
|
1881
|
+
console.print()
|
|
1882
|
+
|
|
1883
|
+
# Mode capabilities
|
|
1884
|
+
console.print(f"[bold {theme_config['secondary_color']}]MODES & CAPABILITIES[/]")
|
|
1885
|
+
console.print()
|
|
1886
|
+
from rich.table import Table
|
|
1887
|
+
table = Table(show_header=True, header_style=f"bold {theme_config['accent_color']}", box=box.SIMPLE)
|
|
1888
|
+
table.add_column("Mode", style=theme_config['primary_color'], width=15)
|
|
1889
|
+
table.add_column("Ask", justify="center", width=8)
|
|
1890
|
+
table.add_column("Read Files", justify="center", width=12)
|
|
1891
|
+
table.add_column("Edit Files", justify="center", width=12)
|
|
1892
|
+
table.add_column("Search", justify="center", width=10)
|
|
1893
|
+
table.add_column("Auto Mode", justify="center", width=10)
|
|
1894
|
+
table.add_column("Sys Cmds", justify="center", width=10)
|
|
1895
|
+
|
|
1896
|
+
table.add_row("Chat", "✓", "✗", "✗", "✗", "✗", "✗")
|
|
1897
|
+
table.add_row("Semi-Agent", "✓", "✓", "✓", "✗", "✗", "✓")
|
|
1898
|
+
table.add_row("Agent", "✓", "✓", "✓", "✓", "✓", "✓")
|
|
1899
|
+
table.add_row("Image Gen", "✓", "✗", "✗", "✗", "✗", "✗")
|
|
1900
|
+
|
|
1901
|
+
console.print(table)
|
|
1902
|
+
console.print()
|
|
1903
|
+
|
|
1904
|
+
# System Commands info for Semi-Agent and Agent modes
|
|
1905
|
+
if mode in ["semi-agent", "agent"]:
|
|
1906
|
+
console.print(f"[bold {theme_config['secondary_color']}]SYSTEM COMMANDS[/]")
|
|
1907
|
+
console.print()
|
|
1908
|
+
console.print(f" [{theme_config['text_color']}]In this mode, Gemini can execute system commands:[/]")
|
|
1909
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Open/close applications (Chrome, Notepad, etc.)")
|
|
1910
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Adjust brightness and volume")
|
|
1911
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Open file explorer")
|
|
1912
|
+
console.print(f" [{theme_config['accent_color']}]•[/] Control media playback")
|
|
1913
|
+
console.print(f" [{theme_config['accent_color']}]•[/] System shutdown (when explicitly requested)")
|
|
1914
|
+
console.print()
|
|
1915
|
+
console.print(f" [dim]Example: 'lower the brightness' or 'open chrome'[/]")
|
|
1916
|
+
console.print()
|
|
1917
|
+
|
|
1918
|
+
# Commands
|
|
1919
|
+
console.print(f"[bold {theme_config['secondary_color']}]AVAILABLE COMMANDS[/]")
|
|
1920
|
+
console.print()
|
|
1921
|
+
console.print(f" [{theme_config['accent_color']}]/help[/] - Show this help message")
|
|
1922
|
+
console.print(f" [{theme_config['accent_color']}]/exit[/] - Exit the current mode")
|
|
1923
|
+
console.print(f" [{theme_config['accent_color']}]/quit[/] - Same as /exit")
|
|
1924
|
+
console.print(f" [{theme_config['accent_color']}]/clear[/] - Clear the screen")
|
|
1925
|
+
console.print(f" [{theme_config['accent_color']}]/mode[/] - Switch between modes")
|
|
1926
|
+
|
|
1927
|
+
if mode in ["semi-agent", "agent"]:
|
|
1928
|
+
console.print(f" [{theme_config['accent_color']}]/status[/] - Show git repository status")
|
|
1929
|
+
console.print(f" [{theme_config['accent_color']}]/commit[/] - Commit changes with AI-generated message")
|
|
1930
|
+
console.print(f" [{theme_config['accent_color']}]/push[/] - Push commits to remote")
|
|
1931
|
+
|
|
1932
|
+
console.print()
|
|
1933
|
+
|
|
1934
|
+
# Settings
|
|
1935
|
+
console.print(f"[bold {theme_config['secondary_color']}]SETTINGS[/]")
|
|
1936
|
+
console.print()
|
|
1937
|
+
console.print(f" [{theme_config['text_color']}]• Theme customization (6 color schemes)[/]")
|
|
1938
|
+
console.print(f" [{theme_config['text_color']}]• Git integration (auto-commit, push)[/]")
|
|
1939
|
+
console.print(f" [{theme_config['text_color']}]• Diff viewer preferences[/]")
|
|
1940
|
+
console.print(f" [{theme_config['text_color']}]• Image generation settings[/]")
|
|
1941
|
+
console.print()
|
|
1942
|
+
console.print(f"[dim]Access via main menu > Settings[/]")
|
|
1943
|
+
console.print()
|
|
1944
|
+
console.rule(style=theme_config['border_color'])
|
|
1945
|
+
console.print()
|
|
1946
|
+
continue
|
|
1947
|
+
|
|
1948
|
+
# Git commands
|
|
1949
|
+
elif user_input.lower() == '/status':
|
|
1950
|
+
console.print()
|
|
1951
|
+
git_info = get_git_status()
|
|
1952
|
+
|
|
1953
|
+
if not git_info['is_repo']:
|
|
1954
|
+
console.print(f"[{theme_config['warning_color']}]⚠ Not a git repository[/]\n")
|
|
1955
|
+
else:
|
|
1956
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Git Status[/]")
|
|
1957
|
+
console.print(f"[{theme_config['text_color']}]Branch:[/] [{theme_config['accent_color']}]{git_info['branch']}[/]")
|
|
1958
|
+
console.print()
|
|
1959
|
+
|
|
1960
|
+
if git_info['has_changes']:
|
|
1961
|
+
console.print(git_info['status'])
|
|
1962
|
+
else:
|
|
1963
|
+
console.print(f"[{theme_config['success_color']}]✓ Working tree clean[/]")
|
|
1964
|
+
console.print()
|
|
1965
|
+
continue
|
|
1966
|
+
|
|
1967
|
+
elif user_input.lower() == '/commit':
|
|
1968
|
+
console.print()
|
|
1969
|
+
git_info = get_git_status()
|
|
1970
|
+
|
|
1971
|
+
if not git_info['is_repo']:
|
|
1972
|
+
console.print(f"[{theme_config['warning_color']}]⚠ Not a git repository[/]\n")
|
|
1973
|
+
continue
|
|
1974
|
+
|
|
1975
|
+
if not git_info['has_changes']:
|
|
1976
|
+
console.print(f"[{theme_config['warning_color']}]⚠ No changes to commit[/]\n")
|
|
1977
|
+
continue
|
|
1978
|
+
|
|
1979
|
+
# Generate commit message using AI
|
|
1980
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Generating commit message...[/]")
|
|
1981
|
+
commit_msg = await generate_commit_message(chat, None)
|
|
1982
|
+
|
|
1983
|
+
if not commit_msg:
|
|
1984
|
+
console.print(f"[{theme_config['warning_color']}]⚠ Failed to generate commit message[/]\n")
|
|
1985
|
+
continue
|
|
1986
|
+
|
|
1987
|
+
console.print()
|
|
1988
|
+
console.print(f"[bold {theme_config['secondary_color']}]◆ Suggested commit message:[/]")
|
|
1989
|
+
console.print(f"[{theme_config['accent_color']}]{commit_msg}[/]")
|
|
1990
|
+
console.print()
|
|
1991
|
+
|
|
1992
|
+
# Ask for confirmation
|
|
1993
|
+
custom_style = Style([
|
|
1994
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
1995
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
1996
|
+
])
|
|
1997
|
+
|
|
1998
|
+
confirm = await questionary.select(
|
|
1999
|
+
"Commit with this message?",
|
|
2000
|
+
choices=['◆ Yes, commit', '◆ No, cancel'],
|
|
2001
|
+
pointer="❯",
|
|
2002
|
+
style=custom_style,
|
|
2003
|
+
qmark="",
|
|
2004
|
+
show_selected=False,
|
|
2005
|
+
use_shortcuts=False
|
|
2006
|
+
).ask_async()
|
|
2007
|
+
|
|
2008
|
+
if confirm == '◆ Yes, commit':
|
|
2009
|
+
success, output = git_commit(commit_msg)
|
|
2010
|
+
if success:
|
|
2011
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Committed successfully[/]")
|
|
2012
|
+
console.print(f"[dim]{output}[/]")
|
|
2013
|
+
else:
|
|
2014
|
+
console.print(f"\n[bold red]✗ Commit failed:[/] {output}")
|
|
2015
|
+
else:
|
|
2016
|
+
console.print(f"\n[{theme_config['warning_color']}]⚠ Commit cancelled[/]")
|
|
2017
|
+
|
|
2018
|
+
console.print()
|
|
2019
|
+
continue
|
|
2020
|
+
|
|
2021
|
+
elif user_input.lower() == '/push':
|
|
2022
|
+
console.print()
|
|
2023
|
+
git_info = get_git_status()
|
|
2024
|
+
|
|
2025
|
+
if not git_info['is_repo']:
|
|
2026
|
+
console.print(f"[{theme_config['warning_color']}]⚠ Not a git repository[/]\n")
|
|
2027
|
+
continue
|
|
2028
|
+
|
|
2029
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Pushing to remote...[/]")
|
|
2030
|
+
success, output = git_push()
|
|
2031
|
+
|
|
2032
|
+
if success:
|
|
2033
|
+
console.print(f"[bold {theme_config['success_color']}]✓ Pushed successfully[/]")
|
|
2034
|
+
console.print(f"[dim]{output}[/]")
|
|
2035
|
+
else:
|
|
2036
|
+
console.print(f"\n[bold red]✗ Push failed:[/] {output}")
|
|
2037
|
+
|
|
2038
|
+
console.print()
|
|
2039
|
+
continue
|
|
2040
|
+
|
|
2041
|
+
# Handle file paths
|
|
2042
|
+
modified_input = user_input
|
|
2043
|
+
referenced_files = []
|
|
2044
|
+
if mode in ["ask", "semi-agent"]:
|
|
2045
|
+
import re
|
|
2046
|
+
path_pattern = r'/([^\s/][^\s]*)'
|
|
2047
|
+
paths = re.findall(path_pattern, user_input)
|
|
2048
|
+
|
|
2049
|
+
if paths:
|
|
2050
|
+
file_contents = []
|
|
2051
|
+
workspace_root = Path.cwd()
|
|
2052
|
+
for path in paths:
|
|
2053
|
+
try:
|
|
2054
|
+
p = workspace_root / path.replace('/', os.sep)
|
|
2055
|
+
|
|
2056
|
+
# Check if it's a directory
|
|
2057
|
+
if p.exists() and p.is_dir():
|
|
2058
|
+
# Get all files in the directory (non-recursive)
|
|
2059
|
+
dir_files = [f for f in p.iterdir() if f.is_file()]
|
|
2060
|
+
if dir_files:
|
|
2061
|
+
for file_path in dir_files:
|
|
2062
|
+
try:
|
|
2063
|
+
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
|
2064
|
+
relative_path = str(file_path.relative_to(workspace_root)).replace(os.sep, '/')
|
|
2065
|
+
file_contents.append(f"\n--- File: {relative_path} ---\n{content}\n--- End of {relative_path} ---")
|
|
2066
|
+
# Track referenced files for semi-agent mode
|
|
2067
|
+
if mode == "semi-agent":
|
|
2068
|
+
referenced_files.append({'path': relative_path, 'full_path': file_path})
|
|
2069
|
+
except Exception as e:
|
|
2070
|
+
console.print(f"[dim]Could not read {file_path.name}: {e}[/]")
|
|
2071
|
+
|
|
2072
|
+
# Check if it's a file
|
|
2073
|
+
elif p.exists() and p.is_file():
|
|
2074
|
+
content = p.read_text(encoding='utf-8', errors='ignore')
|
|
2075
|
+
file_contents.append(f"\n--- File: {path} ---\n{content}\n--- End of {path} ---")
|
|
2076
|
+
# Track referenced files for semi-agent mode
|
|
2077
|
+
if mode == "semi-agent":
|
|
2078
|
+
referenced_files.append({'path': path, 'full_path': p})
|
|
2079
|
+
except Exception as e:
|
|
2080
|
+
console.print(f"[{theme_config['warning_color']}]⚠ Could not access {path}: {e}[/]")
|
|
2081
|
+
|
|
2082
|
+
if file_contents:
|
|
2083
|
+
modified_input = user_input + "\n\n" + "\n".join(file_contents)
|
|
2084
|
+
|
|
2085
|
+
# Add semi-agent or agent mode instructions
|
|
2086
|
+
if mode == "semi-agent":
|
|
2087
|
+
if referenced_files:
|
|
2088
|
+
# Build list of file paths for the prompt
|
|
2089
|
+
file_paths = [ref['path'] for ref in referenced_files]
|
|
2090
|
+
files_list = ', '.join(file_paths)
|
|
2091
|
+
|
|
2092
|
+
modified_input = f"{modified_input}\n\n[STRICT INSTRUCTION - YOU MUST FOLLOW THIS EXACTLY]\nYou are an AI coding agent. Respond with a JSON object containing file modifications/creations AND/OR text replies AND/OR system commands.\n\n⚠️ CRITICAL SAFETY RULES:\n- NEVER delete files or use delete/rm commands\n- NEVER execute destructive system commands\n- Only modify files that are specified or clearly needed\n- For system commands, only use safe operations (open apps, adjust brightness/volume, etc.)\n\nFormat (EXACTLY like this):\n```json\n{{\n \"reply\": \"your text response if user asked a question (optional)\",\n \"{file_paths[0]}\": \"complete file content here\",\n \"new_file.py\": \"content for new file\",\n \"SysPrmpt\": \"system command to execute (optional)\"\n}}\n```\n\nRules:\n- Respond with ONE code block containing ONLY valid JSON\n- Use \"reply\" key ONLY for conversational responses (e.g., answering 'how are you', explanations)\n- Use file paths as keys for file operations (e.g., \"{file_paths[0]}\" for existing, \"new_file.py\" for new)\n- Use \"SysPrmpt\" key for SYSTEM-LEVEL commands like:\n * Opening applications: \"start chrome\" (Windows) or \"open -a 'Google Chrome'\" (Mac)\n * Opening file explorer: \"explorer .\" (Windows) or \"open .\" (Mac)\n * Adjusting brightness (Windows): \"(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1,50)\"\n * Adjusting volume: Use system volume controls\n * Launching media players for music\n * System shutdown ONLY when explicitly requested: \"shutdown /s /t 0\" (Windows)\n * Any other SAFE system commands\n- Values are COMPLETE file contents as strings for files, text for \"reply\", or command string for \"SysPrmpt\"\n- Use \\n for newlines in the JSON strings\n- You can include \"reply\", file operations, AND \"SysPrmpt\" in the same response\n- NO explanations before or after the JSON block\n- NO additional text outside the code block\n\nFiles available: {files_list}\n\nExamples:\n- User: \"lower the brightness\" → {{\"SysPrmpt\": \"powershell -Command '(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1,30)'\", \"reply\": \"Lowering brightness to 30%\"}}\n- User: \"open chrome\" → {{\"SysPrmpt\": \"start chrome\", \"reply\": \"Opening Chrome browser\"}}\n- User: \"open file explorer\" → {{\"SysPrmpt\": \"explorer .\", \"reply\": \"Opening File Explorer\"}}"
|
|
2093
|
+
else:
|
|
2094
|
+
modified_input = f"{modified_input}\n\n[STRICT INSTRUCTION - YOU MUST FOLLOW THIS EXACTLY]\nYou are an AI coding agent. Respond with a JSON object containing file creations AND/OR text replies AND/OR system commands.\n\n⚠️ CRITICAL SAFETY RULES:\n- NEVER delete files or use delete/rm commands\n- NEVER execute destructive system commands\n- Only create/modify files when clearly requested\n- For system commands, only use safe operations (open apps, adjust brightness/volume, etc.)\n\nFormat (EXACTLY like this):\n```json\n{{\n \"reply\": \"your text response if user asked a question (optional)\",\n \"filename.py\": \"complete file content here\",\n \"another_file.py\": \"another complete file content\",\n \"SysPrmpt\": \"system command to execute (optional)\"\n}}\n```\n\nRules:\n- Respond with ONE code block containing ONLY valid JSON\n- Use \"reply\" key ONLY for conversational responses (e.g., answering 'how are you', explanations)\n- Use filenames as keys for file operations (e.g., \"hello.py\", \"script.py\")\n- Use \"SysPrmpt\" key for SYSTEM-LEVEL commands like:\n * Opening applications: \"start chrome\" (Windows) or \"open -a 'Google Chrome'\" (Mac)\n * Opening file explorer: \"explorer .\" (Windows) or \"open .\" (Mac)\n * Adjusting brightness (Windows): \"(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1,50)\"\n * Adjusting volume: Use system volume controls\n * Launching media players for music\n * System shutdown ONLY when explicitly requested: \"shutdown /s /t 0\" (Windows)\n * Any other SAFE system commands\n- Values are COMPLETE file contents as strings for files, text for \"reply\", or command string for \"SysPrmpt\"\n- Use \\n for newlines in the JSON strings\n- You can include \"reply\", file operations, AND \"SysPrmpt\" in the same response\n- NO explanations before or after the JSON block\n- NO additional text outside the code block\n\nExamples:\n- User: \"lower the brightness\" → {{\"SysPrmpt\": \"powershell -Command '(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1,30)'\", \"reply\": \"Lowering brightness to 30%\"}}\n- User: \"open chrome\" → {{\"SysPrmpt\": \"start chrome\", \"reply\": \"Opening Chrome browser\"}}\n- User: \"open file explorer\" → {{\"SysPrmpt\": \"explorer .\", \"reply\": \"Opening File Explorer\"}}"
|
|
2095
|
+
|
|
2096
|
+
elif mode == "agent":
|
|
2097
|
+
# Agent mode: Give full autonomy with search/read/modify capabilities
|
|
2098
|
+
modified_input = f"{modified_input}\n\n[STRICT INSTRUCTION - AUTONOMOUS AGENT MODE]\nYou are an autonomous AI coding agent with workspace access. You can search files, read them, make modifications, AND execute system commands.\n\n⚠️ CRITICAL SAFETY RULES:\n- NEVER delete files or use delete/rm commands\n- NEVER execute destructive system commands\n- Only modify files when clearly needed for the task\n- For system commands, only use safe operations (open apps, adjust brightness/volume, etc.)\n\nAvailable commands (respond with JSON in code block):\n\n1. SEARCH workspace:\n```json\n{{\n \"search\": \"keyword|pattern|regex\",\n \"file_pattern\": \"**/*\",\n \"reply\": \"Searching for database functions...\"\n}}\n```\n⚠️ CRITICAL SEARCH RULES:\n- ALWAYS extract keywords from the user's ACTUAL question\n- If user says \"help bob\" → search for \"bob\", NOT generic \"TODO|FIXME\"\n- If user says \"find apples\" → search for \"apples\", NOT \"error|bug\"\n- Use the EXACT entities/names/topics the user mentioned\n- Only use generic patterns (TODO, FIXME, error) if user EXPLICITLY asks for them\n- file_pattern defaults to \"**/*\" (searches ALL file types: .py, .txt, .md, .json, etc.)\n\n2. READ specific files:\n```json\n{{\n \"read\": [\"/path/to/file1.py\", \"/path/to/file2.py\"],\n \"reply\": \"Reading database connection files...\"\n}}\n```\n\n3. MODIFY/CREATE files:\n```json\n{{\n \"reply\": \"Added error handling to 3 files\",\n \"/path/to/file1.py\": \"complete file content\",\n \"/new_file.py\": \"new file content\"\n}}\n```\n\n4. EXECUTE system commands:\n```json\n{{\n \"SysPrmpt\": \"system command to execute\",\n \"reply\": \"Executing your request...\"\n}}\n```\n\nSystem Command Examples:\n- Open Chrome: {{\"SysPrmpt\": \"start chrome\", \"reply\": \"Opening Chrome\"}}\n- Open File Explorer: {{\"SysPrmpt\": \"explorer .\", \"reply\": \"Opening File Explorer\"}}\n- Lower Brightness (Win): {{\"SysPrmpt\": \"powershell -Command '(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1,30)'\", \"reply\": \"Lowering brightness\"}}\n- Adjust Volume: Use system volume controls\n- Play Music: Launch media player apps\n- System Shutdown (ONLY if explicitly requested): {{\"SysPrmpt\": \"shutdown /s /t 0\", \"reply\": \"Shutting down system\"}}\n\nWorkflow:\n- If you need to find files, use \"search\" command first\n- Extract ACTUAL keywords from user's question for search\n- If you need file contents, use \"read\" command\n- When ready to modify, provide complete file contents\n- For system-level operations (open apps, adjust settings), use \"SysPrmpt\"\n- You can combine commands as needed\n- ONLY search/read if you actually need to - don't search if the task is simple\n\nRules:\n- ONE code block with ONLY valid JSON per response\n- \"reply\" key for status updates (optional)\n- \"SysPrmpt\" key for system commands\n- NO explanations outside the JSON\n- Be efficient - only search/read what you need"
|
|
2099
|
+
|
|
2100
|
+
# Send to Gemini
|
|
2101
|
+
console.print()
|
|
2102
|
+
|
|
2103
|
+
with console.status(f"[bold {theme_config['primary_color']}]◆ Gemini is thinking...[/]", spinner="dots"):
|
|
2104
|
+
response_text, response_images = await send_message(chat, modified_input)
|
|
2105
|
+
|
|
2106
|
+
# Semi-agent or Agent mode: Check for file modifications and extract reply
|
|
2107
|
+
if mode in ["semi-agent", "agent"]:
|
|
2108
|
+
import re
|
|
2109
|
+
import json
|
|
2110
|
+
|
|
2111
|
+
# Look for JSON code block
|
|
2112
|
+
file_modifications = []
|
|
2113
|
+
reply_text = None
|
|
2114
|
+
workspace_root = Path.cwd()
|
|
2115
|
+
|
|
2116
|
+
# Try to find JSON code block
|
|
2117
|
+
json_pattern = r'```json\s*\n(.*?)\n```'
|
|
2118
|
+
json_match = re.search(json_pattern, response_text, re.DOTALL | re.IGNORECASE)
|
|
2119
|
+
|
|
2120
|
+
if json_match:
|
|
2121
|
+
try:
|
|
2122
|
+
json_str = json_match.group(1).strip()
|
|
2123
|
+
response_json = json.loads(json_str)
|
|
2124
|
+
|
|
2125
|
+
# AGENT MODE: Handle search/read commands in a loop
|
|
2126
|
+
if mode == "agent":
|
|
2127
|
+
# Keep processing commands until we get a final reply or file modifications
|
|
2128
|
+
while "search" in response_json or "read" in response_json:
|
|
2129
|
+
# Handle SEARCH command
|
|
2130
|
+
if "search" in response_json:
|
|
2131
|
+
# Extract reply for debugging
|
|
2132
|
+
if "reply" in response_json:
|
|
2133
|
+
reply_text = response_json.get("reply")
|
|
2134
|
+
if reply_text:
|
|
2135
|
+
console.print()
|
|
2136
|
+
console.print(f"[bold {theme_config['secondary_color']}]◆ Gemini[/]")
|
|
2137
|
+
console.print()
|
|
2138
|
+
console.print(f"[dim]{reply_text}[/]")
|
|
2139
|
+
|
|
2140
|
+
search_query = response_json["search"]
|
|
2141
|
+
file_pattern = response_json.get("file_pattern", "**/*")
|
|
2142
|
+
|
|
2143
|
+
console.print()
|
|
2144
|
+
console.print(f"[bold {theme_config['accent_color']}]◆ Searching workspace...[/]")
|
|
2145
|
+
console.print(f"[dim]Query: {search_query}, Pattern: {file_pattern}[/]")
|
|
2146
|
+
|
|
2147
|
+
# Search workspace
|
|
2148
|
+
search_results = search_workspace(search_query, file_pattern)
|
|
2149
|
+
|
|
2150
|
+
if search_results:
|
|
2151
|
+
console.print(f"[{theme_config['success_color']}]✓ Found {len(search_results)} file(s)[/]")
|
|
2152
|
+
console.print()
|
|
2153
|
+
|
|
2154
|
+
# Show results to user
|
|
2155
|
+
for r in search_results:
|
|
2156
|
+
console.print(f" [{theme_config['text_color']}]• {r['path']}[/] [dim]({r['size']}, {r['lines']} lines)[/]")
|
|
2157
|
+
|
|
2158
|
+
# Automatically read all found files
|
|
2159
|
+
file_paths_list = [r['path'] for r in search_results]
|
|
2160
|
+
|
|
2161
|
+
console.print()
|
|
2162
|
+
console.print(f"[bold {theme_config['accent_color']}]◆ Reading {len(file_paths_list)} file(s)...[/]")
|
|
2163
|
+
|
|
2164
|
+
# Read files
|
|
2165
|
+
file_contents_dict = read_workspace_files(file_paths_list)
|
|
2166
|
+
|
|
2167
|
+
# Format contents
|
|
2168
|
+
context = "File contents:\n\n"
|
|
2169
|
+
for path, content in file_contents_dict.items():
|
|
2170
|
+
console.print(f"[dim]✓ Read: {path}[/]")
|
|
2171
|
+
context += f"--- {path} ---\n{content}\n\n"
|
|
2172
|
+
|
|
2173
|
+
# Send contents directly to Gemini - allow iterative search
|
|
2174
|
+
follow_up_prompt = f"{context}\n\n[MANDATORY] Respond with JSON in code block.\n\nYou just read the files above. Now decide:\n\n1. Need MORE info? → {{\"search\": \"keyword\"}} to search again\n2. Have ENOUGH info? → {{\"reply\": \"your answer to user\"}}\n3. Need to UPDATE files? → {{\"filename.txt\": \"content\", \"reply\": \"explanation\"}}\n\nExtract keywords from what you learned. Example: If file mentions 'apple', search for 'apple' or 'apples'.\n\nRespond with ONLY JSON:"
|
|
2175
|
+
|
|
2176
|
+
console.print()
|
|
2177
|
+
with console.status(f"[bold {theme_config['primary_color']}]◆ Gemini is analyzing files...[/]", spinner="dots"):
|
|
2178
|
+
response_text, response_images = await send_message(chat, follow_up_prompt)
|
|
2179
|
+
|
|
2180
|
+
# Process the final response - loop back for more commands
|
|
2181
|
+
json_match = re.search(json_pattern, response_text, re.DOTALL | re.IGNORECASE)
|
|
2182
|
+
if json_match:
|
|
2183
|
+
json_str = json_match.group(1).strip()
|
|
2184
|
+
response_json = json.loads(json_str)
|
|
2185
|
+
|
|
2186
|
+
# Loop back to process search/read/modify commands
|
|
2187
|
+
# The while loop will check if there's another search/read command
|
|
2188
|
+
else:
|
|
2189
|
+
console.print(f"[{theme_config['warning_color']}]⚠ No JSON found in response after reading files[/]")
|
|
2190
|
+
console.print(f"[dim]Full response: {response_text}[/]")
|
|
2191
|
+
console.print()
|
|
2192
|
+
break # Exit if no JSON
|
|
2193
|
+
else:
|
|
2194
|
+
console.print(f"[{theme_config['warning_color']}]⚠ No files found matching query[/]")
|
|
2195
|
+
console.print()
|
|
2196
|
+
break # Exit the command loop
|
|
2197
|
+
|
|
2198
|
+
# Handle READ command
|
|
2199
|
+
elif "read" in response_json:
|
|
2200
|
+
files_to_read = response_json["read"]
|
|
2201
|
+
|
|
2202
|
+
console.print()
|
|
2203
|
+
console.print(f"[bold {theme_config['accent_color']}]◆ Reading {len(files_to_read)} file(s)...[/]")
|
|
2204
|
+
|
|
2205
|
+
# Read files
|
|
2206
|
+
file_contents_dict = read_workspace_files(files_to_read)
|
|
2207
|
+
|
|
2208
|
+
# Format contents
|
|
2209
|
+
context = "File contents:\n\n"
|
|
2210
|
+
for path, content in file_contents_dict.items():
|
|
2211
|
+
console.print(f"[dim]✓ Read: {path}[/]")
|
|
2212
|
+
context += f"--- {path} ---\n{content}\n\n"
|
|
2213
|
+
|
|
2214
|
+
# Send contents back to Gemini - force JSON response
|
|
2215
|
+
follow_up_prompt = f"{context}\n\n[MANDATORY] You can ONLY respond with JSON in a code block. NO other text allowed.\n\nJSON Format Rules:\n```json\n{{\n \"search\": \"keyword\" → Search workspace for this keyword, we return filenames\n \"reply\": \"text\" → Your text response to user\n \"filename.txt\": \"content\" → Create or modify this file with content\n}}\n```\n\nWorkflow:\n1. If you need MORE info → send {{\"search\": \"keyword\"}}\n2. We find files → return filenames to you → you can search AGAIN if needed\n3. When you have ALL info → send {{\"reply\": \"final answer\"}} or {{\"filename\": \"content\"}}\n\nRespond NOW with ONLY JSON:"
|
|
2216
|
+
|
|
2217
|
+
console.print()
|
|
2218
|
+
with console.status(f"[bold {theme_config['primary_color']}]◆ Gemini is deciding next action...[/]", spinner="dots"):
|
|
2219
|
+
response_text, response_images = await send_message(chat, follow_up_prompt)
|
|
2220
|
+
|
|
2221
|
+
# Process the new response - loop back to check for more commands
|
|
2222
|
+
json_match = re.search(json_pattern, response_text, re.DOTALL | re.IGNORECASE)
|
|
2223
|
+
if json_match:
|
|
2224
|
+
json_str = json_match.group(1).strip()
|
|
2225
|
+
response_json = json.loads(json_str)
|
|
2226
|
+
|
|
2227
|
+
# Continue the while loop to process any new search/read/modify commands
|
|
2228
|
+
else:
|
|
2229
|
+
console.print(f"[{theme_config['warning_color']}]⚠ No JSON after read command[/]")
|
|
2230
|
+
break # Exit command loop
|
|
2231
|
+
|
|
2232
|
+
# If we reach here, no more search/read commands to process
|
|
2233
|
+
if "reply" in response_json:
|
|
2234
|
+
reply_text = response_json.pop("reply")
|
|
2235
|
+
else:
|
|
2236
|
+
# Semi-agent mode - extract reply normally
|
|
2237
|
+
if "reply" in response_json:
|
|
2238
|
+
reply_text = response_json.pop("reply")
|
|
2239
|
+
|
|
2240
|
+
# Check for system command execution
|
|
2241
|
+
system_command = None
|
|
2242
|
+
if "SysPrmpt" in response_json:
|
|
2243
|
+
system_command = response_json.pop("SysPrmpt")
|
|
2244
|
+
|
|
2245
|
+
# Process file modifications (works for both semi-agent and agent)
|
|
2246
|
+
for file_path, new_content in response_json.items():
|
|
2247
|
+
# Skip non-file keys
|
|
2248
|
+
if file_path in ["search", "read", "file_pattern"]:
|
|
2249
|
+
continue
|
|
2250
|
+
|
|
2251
|
+
# Normalize the path (remove leading slash if present)
|
|
2252
|
+
normalized_path = file_path.lstrip('/')
|
|
2253
|
+
|
|
2254
|
+
# Check if it's an existing referenced file (semi-agent mode)
|
|
2255
|
+
matched = False
|
|
2256
|
+
if mode == "semi-agent":
|
|
2257
|
+
for ref_file in referenced_files:
|
|
2258
|
+
ref_path = ref_file['path'].lstrip('/')
|
|
2259
|
+
if ref_path == normalized_path or ref_file['path'] == file_path:
|
|
2260
|
+
file_modifications.append({
|
|
2261
|
+
'path': ref_file['path'],
|
|
2262
|
+
'full_path': ref_file['full_path'],
|
|
2263
|
+
'new_content': new_content,
|
|
2264
|
+
'is_new': False
|
|
2265
|
+
})
|
|
2266
|
+
matched = True
|
|
2267
|
+
break
|
|
2268
|
+
|
|
2269
|
+
# For agent mode or unmatched files, treat as new/existing file
|
|
2270
|
+
if not matched:
|
|
2271
|
+
file_full_path = workspace_root / normalized_path
|
|
2272
|
+
is_new = not file_full_path.exists()
|
|
2273
|
+
file_modifications.append({
|
|
2274
|
+
'path': normalized_path,
|
|
2275
|
+
'full_path': file_full_path,
|
|
2276
|
+
'new_content': new_content,
|
|
2277
|
+
'is_new': is_new
|
|
2278
|
+
})
|
|
2279
|
+
except json.JSONDecodeError as e:
|
|
2280
|
+
console.print(f"\\n[{theme_config['warning_color']}]⚠ Failed to parse JSON response: {e}[/]")
|
|
2281
|
+
|
|
2282
|
+
# Display response - either reply text or full response
|
|
2283
|
+
console.print()
|
|
2284
|
+
console.print(f"[bold {theme_config['secondary_color']}]◆ Gemini[/]")
|
|
2285
|
+
console.print()
|
|
2286
|
+
|
|
2287
|
+
if reply_text:
|
|
2288
|
+
# Display extracted reply text
|
|
2289
|
+
console.print(Markdown(reply_text))
|
|
2290
|
+
elif not file_modifications:
|
|
2291
|
+
# No reply key and no file modifications - show full response
|
|
2292
|
+
console.print(Markdown(response_text))
|
|
2293
|
+
# If only file modifications without reply, skip displaying full response
|
|
2294
|
+
|
|
2295
|
+
# Execute system command if present
|
|
2296
|
+
if 'system_command' in locals() and system_command:
|
|
2297
|
+
console.print()
|
|
2298
|
+
console.print(f"[bold {theme_config['accent_color']}]◆ Executing System Command[/]")
|
|
2299
|
+
console.print(f"[dim]Command: {system_command}[/]")
|
|
2300
|
+
console.print()
|
|
2301
|
+
|
|
2302
|
+
# Execute the command
|
|
2303
|
+
success, output = execute_system_command(system_command)
|
|
2304
|
+
|
|
2305
|
+
if success:
|
|
2306
|
+
console.print(f"[bold {theme_config['success_color']}]✓ Command executed successfully[/]")
|
|
2307
|
+
if output:
|
|
2308
|
+
console.print(f"[dim]{output}[/]")
|
|
2309
|
+
else:
|
|
2310
|
+
console.print(f"[bold red]✗ Command failed[/]")
|
|
2311
|
+
console.print(f"[dim]{output}[/]")
|
|
2312
|
+
console.print()
|
|
2313
|
+
|
|
2314
|
+
# If modifications were found, show diffs and ask to apply them
|
|
2315
|
+
if file_modifications:
|
|
2316
|
+
console.print()
|
|
2317
|
+
console.print(f"[bold {theme_config['accent_color']}]◆ File Modifications Detected[/]")
|
|
2318
|
+
console.print(f"[dim]Found {len(file_modifications)} file(s) to modify:[/]")
|
|
2319
|
+
for mod in file_modifications:
|
|
2320
|
+
status = "NEW" if mod['is_new'] else "MODIFIED"
|
|
2321
|
+
console.print(f" [{theme_config['primary_color']}]•[/] {mod['path']} [{theme_config['success_color'] if mod['is_new'] else theme_config['accent_color']}]({status})[/]")
|
|
2322
|
+
console.print()
|
|
2323
|
+
|
|
2324
|
+
# Open VS Code diff editors for all files
|
|
2325
|
+
temp_files = []
|
|
2326
|
+
for mod in file_modifications:
|
|
2327
|
+
tmp_path = open_vscode_diff(
|
|
2328
|
+
mod['full_path'],
|
|
2329
|
+
mod['new_content'],
|
|
2330
|
+
mod['path'],
|
|
2331
|
+
mod['is_new']
|
|
2332
|
+
)
|
|
2333
|
+
if tmp_path:
|
|
2334
|
+
temp_files.append(tmp_path)
|
|
2335
|
+
|
|
2336
|
+
# Create custom style for confirmation
|
|
2337
|
+
custom_style = Style([
|
|
2338
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
2339
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
2340
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
2341
|
+
('instruction', 'fg:#555555'),
|
|
2342
|
+
])
|
|
2343
|
+
|
|
2344
|
+
apply_changes = await questionary.select(
|
|
2345
|
+
"Apply these changes to the files?",
|
|
2346
|
+
choices=["◆ Yes, apply changes", "◆ No, skip"],
|
|
2347
|
+
pointer="❯",
|
|
2348
|
+
style=custom_style,
|
|
2349
|
+
qmark="",
|
|
2350
|
+
instruction="(Use arrow keys)",
|
|
2351
|
+
show_selected=False,
|
|
2352
|
+
use_shortcuts=False
|
|
2353
|
+
).ask_async()
|
|
2354
|
+
|
|
2355
|
+
# Clean up temp files
|
|
2356
|
+
for tmp_file in temp_files:
|
|
2357
|
+
try:
|
|
2358
|
+
os.unlink(tmp_file)
|
|
2359
|
+
except:
|
|
2360
|
+
pass
|
|
2361
|
+
|
|
2362
|
+
if apply_changes == "◆ Yes, apply changes":
|
|
2363
|
+
modified_files = []
|
|
2364
|
+
for mod in file_modifications:
|
|
2365
|
+
try:
|
|
2366
|
+
# Create parent directories if needed for new files
|
|
2367
|
+
if mod['is_new']:
|
|
2368
|
+
mod['full_path'].parent.mkdir(parents=True, exist_ok=True)
|
|
2369
|
+
|
|
2370
|
+
# Write new content
|
|
2371
|
+
mod['full_path'].write_text(mod['new_content'], encoding='utf-8')
|
|
2372
|
+
|
|
2373
|
+
action = "Created" if mod['is_new'] else "Modified"
|
|
2374
|
+
console.print(f"[bold {theme_config['success_color']}]✓ {action}: {mod['path']}[/]")
|
|
2375
|
+
modified_files.append(mod['path'])
|
|
2376
|
+
except Exception as e:
|
|
2377
|
+
console.print(f"[bold red]✗[/] Failed to {'create' if mod['is_new'] else 'modify'} {mod['path']}: {e}")
|
|
2378
|
+
console.print()
|
|
2379
|
+
|
|
2380
|
+
# Offer to commit changes based on preferences
|
|
2381
|
+
if git_preferences['enabled'] and git_preferences['commit_mode'] == 'immediate':
|
|
2382
|
+
git_info = get_git_status()
|
|
2383
|
+
if git_info['is_repo'] and git_info['has_changes']:
|
|
2384
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Would you like to commit these changes?[/]\n")
|
|
2385
|
+
|
|
2386
|
+
custom_style = Style([
|
|
2387
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
2388
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
2389
|
+
])
|
|
2390
|
+
|
|
2391
|
+
commit_choice = await questionary.select(
|
|
2392
|
+
"",
|
|
2393
|
+
choices=['◆ Yes, generate commit message', '◆ No, skip'],
|
|
2394
|
+
pointer="❯",
|
|
2395
|
+
style=custom_style,
|
|
2396
|
+
qmark="",
|
|
2397
|
+
show_selected=False,
|
|
2398
|
+
use_shortcuts=False
|
|
2399
|
+
).ask_async()
|
|
2400
|
+
|
|
2401
|
+
if commit_choice == '◆ Yes, generate commit message':
|
|
2402
|
+
console.print()
|
|
2403
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Generating commit message...[/]")
|
|
2404
|
+
commit_msg = await generate_commit_message(chat, modified_files)
|
|
2405
|
+
|
|
2406
|
+
if commit_msg:
|
|
2407
|
+
console.print()
|
|
2408
|
+
console.print(f"[bold {theme_config['secondary_color']}]◆ Suggested commit message:[/]")
|
|
2409
|
+
console.print(f"[{theme_config['accent_color']}]{commit_msg}[/]")
|
|
2410
|
+
console.print()
|
|
2411
|
+
|
|
2412
|
+
confirm = await questionary.select(
|
|
2413
|
+
"Commit with this message?",
|
|
2414
|
+
choices=['◆ Yes, commit', '◆ No, cancel'],
|
|
2415
|
+
pointer="❯",
|
|
2416
|
+
style=custom_style,
|
|
2417
|
+
qmark="",
|
|
2418
|
+
show_selected=False,
|
|
2419
|
+
use_shortcuts=False
|
|
2420
|
+
).ask_async()
|
|
2421
|
+
|
|
2422
|
+
if confirm == '◆ Yes, commit':
|
|
2423
|
+
success, output = git_commit(commit_msg, modified_files)
|
|
2424
|
+
if success:
|
|
2425
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Committed successfully[/]")
|
|
2426
|
+
console.print(f"[dim]{output}[/]")
|
|
2427
|
+
|
|
2428
|
+
# Auto-push if enabled
|
|
2429
|
+
if git_preferences['auto_push']:
|
|
2430
|
+
console.print(f"[bold {theme_config['primary_color']}]◆ Pushing to remote...[/]")
|
|
2431
|
+
push_success, push_output = git_push()
|
|
2432
|
+
if push_success:
|
|
2433
|
+
console.print(f"[bold {theme_config['success_color']}]✓ Pushed successfully[/]")
|
|
2434
|
+
else:
|
|
2435
|
+
console.print(f"[bold red]✗ Push failed:[/] {push_output}")
|
|
2436
|
+
console.print()
|
|
2437
|
+
else:
|
|
2438
|
+
console.print(f"\n[bold red]✗ Commit failed:[/] {output}\n")
|
|
2439
|
+
else:
|
|
2440
|
+
console.print(f"\n[{theme_config['warning_color']}]⚠ Commit cancelled[/]\n")
|
|
2441
|
+
else:
|
|
2442
|
+
console.print(f"\n[{theme_config['warning_color']}]⚠ Failed to generate commit message[/]\n")
|
|
2443
|
+
else:
|
|
2444
|
+
console.print(f"[{theme_config['warning_color']}]⚠ Changes not applied[/]\n")
|
|
2445
|
+
else:
|
|
2446
|
+
# Not in semi-agent mode - display response normally
|
|
2447
|
+
console.print()
|
|
2448
|
+
console.print(f"[bold {theme_config['secondary_color']}]◆ Gemini[/]")
|
|
2449
|
+
console.print()
|
|
2450
|
+
console.print(Markdown(response_text))
|
|
2451
|
+
|
|
2452
|
+
# Display images if any
|
|
2453
|
+
if response_images:
|
|
2454
|
+
images_dir = image_settings['save_path']
|
|
2455
|
+
images_dir.mkdir(exist_ok=True)
|
|
2456
|
+
|
|
2457
|
+
for idx, img_data in enumerate(response_images, 1):
|
|
2458
|
+
try:
|
|
2459
|
+
# Generate unique filename
|
|
2460
|
+
import time
|
|
2461
|
+
img_filename = f'image_{int(time.time())}_{idx}.png'
|
|
2462
|
+
img_path = images_dir / img_filename
|
|
2463
|
+
|
|
2464
|
+
# Download from URL with cookies (this method works)
|
|
2465
|
+
if hasattr(img_data, 'url'):
|
|
2466
|
+
import requests
|
|
2467
|
+
url = img_data.url
|
|
2468
|
+
cookies = img_data.cookies if hasattr(img_data, 'cookies') else {}
|
|
2469
|
+
|
|
2470
|
+
resp = requests.get(url, cookies=cookies, stream=True)
|
|
2471
|
+
resp.raise_for_status()
|
|
2472
|
+
|
|
2473
|
+
img_path.write_bytes(resp.content)
|
|
2474
|
+
img_abs_path = img_path.absolute()
|
|
2475
|
+
|
|
2476
|
+
# Open image in VS Code/IDE viewer
|
|
2477
|
+
console.print(f"[{theme_config['accent_color']}]✓ Saved:[/] {img_abs_path}\n")
|
|
2478
|
+
webbrowser.open(str(img_abs_path))
|
|
2479
|
+
|
|
2480
|
+
except Exception as e:
|
|
2481
|
+
console.print(f"[red]Failed to process image {idx}: {e}[/]\n")
|
|
2482
|
+
|
|
2483
|
+
console.print() # Just add some spacing
|
|
2484
|
+
|
|
2485
|
+
except KeyboardInterrupt:
|
|
2486
|
+
# User pressed Ctrl+C in the main loop - handle git commits for semi-agent/agent
|
|
2487
|
+
if mode in ["semi-agent", "agent"] and git_preferences['enabled'] and git_preferences['commit_mode'] == 'on_exit':
|
|
2488
|
+
await handle_exit_git_commit(chat)
|
|
2489
|
+
console.print(f"\n\n[bold {theme_config['primary_color']}]◆ Goodbye! ◆[/]\n")
|
|
2490
|
+
break
|
|
2491
|
+
|
|
2492
|
+
except Exception as e:
|
|
2493
|
+
# Catch any other errors but don't exit
|
|
2494
|
+
import traceback
|
|
2495
|
+
console.print(f"\n[{theme_config['warning_color']}]⚠ Unexpected Error: {e}[/]")
|
|
2496
|
+
console.print(f"[dim]Traceback:\n{traceback.format_exc()}[/]")
|
|
2497
|
+
continue
|
|
2498
|
+
|
|
2499
|
+
|
|
2500
|
+
async def customize_theme():
|
|
2501
|
+
"""Allow user to customize theme colors."""
|
|
2502
|
+
clear_screen()
|
|
2503
|
+
print_banner()
|
|
2504
|
+
|
|
2505
|
+
console.print(f"\n[bold {theme_config['primary_color']}]Theme Customization[/]")
|
|
2506
|
+
console.rule(style=theme_config['border_color'])
|
|
2507
|
+
|
|
2508
|
+
# Vibrant color options
|
|
2509
|
+
colors = [
|
|
2510
|
+
('#FFFFFF', '◆ White (Default)'),
|
|
2511
|
+
('#00FFD4', '◆ Cyan/Turquoise'),
|
|
2512
|
+
('#FF6B9D', '◆ Vibrant Pink'),
|
|
2513
|
+
('#FFD700', '◆ Gold'),
|
|
2514
|
+
('#00FF88', '◆ Neon Green'),
|
|
2515
|
+
('#9D4EDD', '◆ Purple'),
|
|
2516
|
+
('#FF006E', '◆ Hot Pink')
|
|
2517
|
+
]
|
|
2518
|
+
|
|
2519
|
+
console.print()
|
|
2520
|
+
console.print(f"[#FFFFFF]◆ White[/] [#00FFD4]◆ Cyan[/] [#FF6B9D]◆ Pink[/] [#FFD700]◆ Gold[/] [#00FF88]◆ Green[/] [#9D4EDD]◆ Purple[/] [#FF006E]◆ Hot Pink[/]")
|
|
2521
|
+
console.print()
|
|
2522
|
+
|
|
2523
|
+
# Create custom style
|
|
2524
|
+
custom_style = Style([
|
|
2525
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
2526
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
2527
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
2528
|
+
('instruction', 'fg:#555555'), # Dim gray for instruction text
|
|
2529
|
+
])
|
|
2530
|
+
|
|
2531
|
+
choice_list = [name for _, name in colors]
|
|
2532
|
+
|
|
2533
|
+
selected_name = await questionary.select(
|
|
2534
|
+
"Select Theme Color:",
|
|
2535
|
+
choices=choice_list,
|
|
2536
|
+
pointer="❯",
|
|
2537
|
+
style=custom_style,
|
|
2538
|
+
qmark="",
|
|
2539
|
+
instruction="(Use arrow keys)",
|
|
2540
|
+
show_selected=False,
|
|
2541
|
+
use_shortcuts=False
|
|
2542
|
+
).ask_async()
|
|
2543
|
+
|
|
2544
|
+
if selected_name:
|
|
2545
|
+
for color_code, color_name in colors:
|
|
2546
|
+
if color_name == selected_name:
|
|
2547
|
+
theme_config['primary_color'] = color_code
|
|
2548
|
+
theme_config['accent_color'] = color_code
|
|
2549
|
+
break
|
|
2550
|
+
|
|
2551
|
+
|
|
2552
|
+
# =============================================================================
|
|
2553
|
+
# Main
|
|
2554
|
+
# =============================================================================
|
|
2555
|
+
|
|
2556
|
+
async def main():
|
|
2557
|
+
"""Main entry point."""
|
|
2558
|
+
|
|
2559
|
+
print_banner()
|
|
2560
|
+
|
|
2561
|
+
# Create custom style with vibrant colors
|
|
2562
|
+
custom_style = Style([
|
|
2563
|
+
('pointer', f'fg:{theme_config["accent_color"]} bold'),
|
|
2564
|
+
('highlighted', f'fg:{theme_config["primary_color"]} bold'),
|
|
2565
|
+
('question', f'fg:{theme_config["secondary_color"]} bold'),
|
|
2566
|
+
('selected', f'fg:{theme_config["accent_color"]} bold'),
|
|
2567
|
+
('instruction', 'fg:#555555'), # Dim gray for instruction text
|
|
2568
|
+
])
|
|
2569
|
+
|
|
2570
|
+
# Initial menu with diamond indicators
|
|
2571
|
+
console.print()
|
|
2572
|
+
choice = await questionary.select(
|
|
2573
|
+
"",
|
|
2574
|
+
choices=["◆ Connect to Gemini", "◆ Customize Theme", "◆ Settings", "◆ Exit"],
|
|
2575
|
+
pointer="❯",
|
|
2576
|
+
style=custom_style,
|
|
2577
|
+
qmark="",
|
|
2578
|
+
instruction="Choose an option (Use arrow keys)",
|
|
2579
|
+
show_selected=False,
|
|
2580
|
+
use_shortcuts=False
|
|
2581
|
+
).ask_async()
|
|
2582
|
+
|
|
2583
|
+
if choice == "◆ Exit":
|
|
2584
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Goodbye! ◆[/]\n")
|
|
2585
|
+
return
|
|
2586
|
+
elif choice == "◆ Customize Theme":
|
|
2587
|
+
await customize_theme()
|
|
2588
|
+
# Restart from beginning with updated theme
|
|
2589
|
+
await main()
|
|
2590
|
+
return
|
|
2591
|
+
elif choice == "◆ Settings":
|
|
2592
|
+
await settings_menu()
|
|
2593
|
+
# Restart from beginning
|
|
2594
|
+
await main()
|
|
2595
|
+
return
|
|
2596
|
+
|
|
2597
|
+
# Safety message after choosing to connect
|
|
2598
|
+
clear_screen()
|
|
2599
|
+
print_banner()
|
|
2600
|
+
|
|
2601
|
+
# Display big safety message with ASCII art and gradient
|
|
2602
|
+
safety_lines = [
|
|
2603
|
+
"███████╗ █████╗ ███████╗███████╗",
|
|
2604
|
+
"██╔════╝██╔══██╗██╔════╝██╔════╝",
|
|
2605
|
+
"███████╗███████║█████╗ █████╗ ",
|
|
2606
|
+
"╚════██║██╔══██║██╔══╝ ██╔══╝ ",
|
|
2607
|
+
"███████║██║ ██║██║ ███████╗",
|
|
2608
|
+
"╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝"
|
|
2609
|
+
]
|
|
2610
|
+
|
|
2611
|
+
# Gradient colors from white to black
|
|
2612
|
+
safety_gradient = ['#FFFFFF', '#DDDDDD', '#BBBBBB', '#999999', '#777777', '#555555']
|
|
2613
|
+
|
|
2614
|
+
console.print()
|
|
2615
|
+
for i, line in enumerate(safety_lines):
|
|
2616
|
+
console.print(line, style=f"bold {safety_gradient[i]}", justify="center")
|
|
2617
|
+
|
|
2618
|
+
console.print()
|
|
2619
|
+
from rich.text import Text
|
|
2620
|
+
line1 = Text("This is a CLIENT-SIDE application with NO SERVER.", style=theme_config['text_color'])
|
|
2621
|
+
line2 = Text("Your data stays on YOUR machine. 100% Private & Secure.", style=theme_config['text_color'])
|
|
2622
|
+
console.print(line1, justify="center")
|
|
2623
|
+
console.print(line2, justify="center")
|
|
2624
|
+
console.print()
|
|
2625
|
+
|
|
2626
|
+
# Check for saved cookies
|
|
2627
|
+
saved_psid, saved_psidts = load_cookies()
|
|
2628
|
+
|
|
2629
|
+
if saved_psid:
|
|
2630
|
+
console.print(f"\n[bold {theme_config['primary_color']}]Found saved cookies[/]", justify="center")
|
|
2631
|
+
console.print()
|
|
2632
|
+
|
|
2633
|
+
use_saved = await questionary.select(
|
|
2634
|
+
"Use saved cookies?",
|
|
2635
|
+
choices=["◆ Yes", "◆ No"],
|
|
2636
|
+
pointer="❯",
|
|
2637
|
+
style=custom_style,
|
|
2638
|
+
qmark="",
|
|
2639
|
+
instruction="(Use arrow keys)",
|
|
2640
|
+
show_selected=False,
|
|
2641
|
+
use_shortcuts=False
|
|
2642
|
+
).ask_async()
|
|
2643
|
+
|
|
2644
|
+
if use_saved == "◆ Yes":
|
|
2645
|
+
psid, psidts = saved_psid, saved_psidts
|
|
2646
|
+
console.print(f"\n[bold {theme_config['success_color']}]✓ Using saved cookies[/]\n")
|
|
2647
|
+
else:
|
|
2648
|
+
psid, psidts = get_cookies()
|
|
2649
|
+
if psid:
|
|
2650
|
+
save_cookies(psid, psidts)
|
|
2651
|
+
else:
|
|
2652
|
+
psid, psidts = get_cookies()
|
|
2653
|
+
if psid:
|
|
2654
|
+
save_cookies(psid, psidts)
|
|
2655
|
+
|
|
2656
|
+
if not psid:
|
|
2657
|
+
return
|
|
2658
|
+
|
|
2659
|
+
# Initialize client
|
|
2660
|
+
client = await initialize_client(psid, psidts)
|
|
2661
|
+
|
|
2662
|
+
if not client:
|
|
2663
|
+
console.print(f"\n[bold red]Failed to connect to Gemini[/]")
|
|
2664
|
+
return
|
|
2665
|
+
|
|
2666
|
+
# Mode selection loop
|
|
2667
|
+
while True:
|
|
2668
|
+
clear_screen()
|
|
2669
|
+
print_banner()
|
|
2670
|
+
|
|
2671
|
+
console.print()
|
|
2672
|
+
mode_choice = await questionary.select(
|
|
2673
|
+
"Select mode:",
|
|
2674
|
+
choices=["◆ Ask", "◆ Semi-Agent", "◆ Agent (Autonomous)", "◆ Generate Images", "◆ Exit"],
|
|
2675
|
+
pointer="❯",
|
|
2676
|
+
style=custom_style,
|
|
2677
|
+
qmark="",
|
|
2678
|
+
instruction="Choose a mode (Use arrow keys)",
|
|
2679
|
+
show_selected=False,
|
|
2680
|
+
use_shortcuts=False
|
|
2681
|
+
).ask_async()
|
|
2682
|
+
|
|
2683
|
+
if mode_choice == "◆ Exit":
|
|
2684
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Goodbye! ◆[/]\n")
|
|
2685
|
+
break
|
|
2686
|
+
|
|
2687
|
+
# Map choice to mode
|
|
2688
|
+
mode_map = {
|
|
2689
|
+
"◆ Ask": "ask",
|
|
2690
|
+
"◆ Semi-Agent": "semi-agent",
|
|
2691
|
+
"◆ Agent (Autonomous)": "agent",
|
|
2692
|
+
"◆ Generate Images": "image"
|
|
2693
|
+
}
|
|
2694
|
+
mode = mode_map.get(mode_choice, "ask")
|
|
2695
|
+
|
|
2696
|
+
# Start chat in selected mode
|
|
2697
|
+
clear_screen()
|
|
2698
|
+
print_banner()
|
|
2699
|
+
await chat_loop(client, mode)
|
|
2700
|
+
|
|
2701
|
+
try:
|
|
2702
|
+
await client.close()
|
|
2703
|
+
except:
|
|
2704
|
+
pass
|
|
2705
|
+
|
|
2706
|
+
|
|
2707
|
+
def run_cli():
|
|
2708
|
+
"""Entry point for the CLI tool."""
|
|
2709
|
+
try:
|
|
2710
|
+
asyncio.run(main())
|
|
2711
|
+
except KeyboardInterrupt:
|
|
2712
|
+
console.print(f"\n[bold {theme_config['primary_color']}]◆ Goodbye! ◆[/]")
|
|
2713
|
+
sys.exit(0)
|
|
2714
|
+
|
|
2715
|
+
|
|
2716
|
+
if __name__ == "__main__":
|
|
2717
|
+
run_cli()
|