cobalt-cli-linux 0.1.6__tar.gz → 0.1.7__tar.gz
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.
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/PKG-INFO +1 -1
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/pyproject.toml +1 -1
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/groq_client.py +6 -2
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/tui.py +79 -3
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/PKG-INFO +1 -1
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/tests/test_cli.py +46 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/README.md +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/setup.cfg +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/ASCII.txt +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/__init__.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/__main__.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/agent.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/cli.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/config.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/deepseek_client.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/executor.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/history.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux/profiles.py +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/SOURCES.txt +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/dependency_links.txt +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/entry_points.txt +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/requires.txt +0 -0
- {cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/top_level.txt +0 -0
|
@@ -16,6 +16,7 @@ class GroqStreamEvent(dict[str, str]):
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class GroqClient:
|
|
19
|
+
MAX_MODEL_TOKENS = 512
|
|
19
20
|
DEFAULT_MODELS: ClassVar[list[str]] = [
|
|
20
21
|
"llama-3.3-70b-versatile",
|
|
21
22
|
"llama-3.1-8b-instant",
|
|
@@ -46,6 +47,9 @@ class GroqClient:
|
|
|
46
47
|
lowered = model_id.lower()
|
|
47
48
|
return not any(token in lowered for token in self.NON_CHAT_TOKENS)
|
|
48
49
|
|
|
50
|
+
def _safe_max_tokens(self, max_tokens: int) -> int:
|
|
51
|
+
return max(1, min(max_tokens, self.MAX_MODEL_TOKENS))
|
|
52
|
+
|
|
49
53
|
def resolve_model(self) -> str:
|
|
50
54
|
if self.model and self.model != "auto":
|
|
51
55
|
return self.model
|
|
@@ -85,7 +89,7 @@ class GroqClient:
|
|
|
85
89
|
"model": resolved_model,
|
|
86
90
|
"messages": messages,
|
|
87
91
|
"temperature": temperature,
|
|
88
|
-
"max_tokens": max_tokens,
|
|
92
|
+
"max_tokens": self._safe_max_tokens(max_tokens),
|
|
89
93
|
}
|
|
90
94
|
headers = {
|
|
91
95
|
"Authorization": f"Bearer {self.api_key}",
|
|
@@ -135,7 +139,7 @@ class GroqClient:
|
|
|
135
139
|
"model": resolved_model,
|
|
136
140
|
"messages": messages,
|
|
137
141
|
"temperature": temperature,
|
|
138
|
-
"max_tokens": max_tokens,
|
|
142
|
+
"max_tokens": self._safe_max_tokens(max_tokens),
|
|
139
143
|
"stream": True,
|
|
140
144
|
}
|
|
141
145
|
headers = {
|
|
@@ -32,6 +32,7 @@ class CobaltTUI:
|
|
|
32
32
|
self.auth_view: str | None = None
|
|
33
33
|
self.auth_field = 0
|
|
34
34
|
self.auth_values = ["", ""]
|
|
35
|
+
self.scroll_offset = 0
|
|
35
36
|
|
|
36
37
|
def _load_ascii_art(self) -> list[str]:
|
|
37
38
|
ascii_path = Path(__file__).resolve().parents[2] / "ASCII.txt"
|
|
@@ -93,6 +94,7 @@ class CobaltTUI:
|
|
|
93
94
|
self.history = self.current_chat
|
|
94
95
|
self.selected_chat_path = str(self.current_chat.path)
|
|
95
96
|
self.chat_files = self.chat_store.list_chats()
|
|
97
|
+
self.scroll_offset = 0
|
|
96
98
|
|
|
97
99
|
def _delete_selected_chat(self) -> None:
|
|
98
100
|
if not self.selected_chat_path:
|
|
@@ -106,6 +108,7 @@ class CobaltTUI:
|
|
|
106
108
|
self.selected_chat_path = str(self.chat_files[-1])
|
|
107
109
|
self.history = self.chat_store.load_chat(self.selected_chat_path)
|
|
108
110
|
self.current_chat = self.history
|
|
111
|
+
self.scroll_offset = 0
|
|
109
112
|
else:
|
|
110
113
|
self._start_new_chat()
|
|
111
114
|
|
|
@@ -171,6 +174,9 @@ class CobaltTUI:
|
|
|
171
174
|
stdscr.addstr(box_y + 1 + index, box_x, f"{prompt}{line}"[:box_width])
|
|
172
175
|
copy_label = "[ Copy prompt ]"
|
|
173
176
|
copy_x = max(box_x + 4, box_x + box_width - len(copy_label) - 2)
|
|
177
|
+
paste_label = "[ Paste ]"
|
|
178
|
+
paste_x = max(box_x + 4, copy_x - len(paste_label) - 2)
|
|
179
|
+
stdscr.addstr(box_y + 1, paste_x, paste_label[: max(0, copy_x - paste_x - 2)], curses.A_DIM)
|
|
174
180
|
stdscr.addstr(box_y + 1, copy_x, copy_label[: max(0, box_width - (copy_x - box_x))], curses.A_DIM)
|
|
175
181
|
stdscr.addstr(box_y + box_height - 1, box_x, bottom[:box_width], curses.A_BOLD if focused else curses.A_DIM)
|
|
176
182
|
|
|
@@ -186,10 +192,30 @@ class CobaltTUI:
|
|
|
186
192
|
args.extend(["-selection", "clipboard"])
|
|
187
193
|
elif command == "xsel":
|
|
188
194
|
args.extend(["--clipboard", "--input"])
|
|
189
|
-
|
|
195
|
+
try:
|
|
196
|
+
subprocess.run(args, input=text, text=True, check=True)
|
|
197
|
+
except (OSError, subprocess.CalledProcessError):
|
|
198
|
+
continue
|
|
190
199
|
return "Copied"
|
|
191
200
|
raise RuntimeError("No clipboard utility found (install wl-clipboard, xclip, or xsel)")
|
|
192
201
|
|
|
202
|
+
def _paste_from_clipboard(self) -> str:
|
|
203
|
+
for command in ("wl-paste", "xclip", "xsel"):
|
|
204
|
+
executable = shutil.which(command)
|
|
205
|
+
if not executable:
|
|
206
|
+
continue
|
|
207
|
+
args = [executable]
|
|
208
|
+
if command == "xclip":
|
|
209
|
+
args.extend(["-selection", "clipboard", "-o"])
|
|
210
|
+
elif command == "xsel":
|
|
211
|
+
args.extend(["--clipboard", "--output"])
|
|
212
|
+
try:
|
|
213
|
+
result = subprocess.run(args, capture_output=True, text=True, check=True)
|
|
214
|
+
except (OSError, subprocess.CalledProcessError):
|
|
215
|
+
continue
|
|
216
|
+
return result.stdout.replace("\r\n", "\n").replace("\r", "\n")
|
|
217
|
+
raise RuntimeError("No working clipboard utility found (install wl-clipboard, xclip, or xsel)")
|
|
218
|
+
|
|
193
219
|
def _latest_response(self) -> str:
|
|
194
220
|
for message in reversed(self.history.messages):
|
|
195
221
|
if message.get("role") == "assistant":
|
|
@@ -305,7 +331,9 @@ class CobaltTUI:
|
|
|
305
331
|
for message_index, message in enumerate(self.history.latest(18)):
|
|
306
332
|
formatted_rows.extend((line, attr, message_index) for line, attr in self._format_message_lines(message, main_w - 8, message_index))
|
|
307
333
|
|
|
308
|
-
|
|
334
|
+
content_height = max(1, box_y - main_y - 2)
|
|
335
|
+
end = max(0, len(formatted_rows) - self.scroll_offset)
|
|
336
|
+
visible = formatted_rows[max(0, end - content_height):end]
|
|
309
337
|
for idx, (line, attr, _) in enumerate(visible[: main_h - 4]):
|
|
310
338
|
y = main_y + 1 + idx
|
|
311
339
|
if y >= box_y:
|
|
@@ -371,6 +399,7 @@ class CobaltTUI:
|
|
|
371
399
|
self.selected_chat_path = str(target)
|
|
372
400
|
self.history = self.chat_store.load_chat(target)
|
|
373
401
|
self.current_chat = self.history
|
|
402
|
+
self.scroll_offset = 0
|
|
374
403
|
return True
|
|
375
404
|
return False
|
|
376
405
|
|
|
@@ -399,6 +428,19 @@ class CobaltTUI:
|
|
|
399
428
|
return "response"
|
|
400
429
|
return None
|
|
401
430
|
|
|
431
|
+
def _paste_button_at(self, x: int, y: int, input_text: str) -> bool:
|
|
432
|
+
height, width = curses.LINES, curses.COLS
|
|
433
|
+
sidebar_w = min(26, max(20, width // 4))
|
|
434
|
+
main_x, main_y = 3 + sidebar_w, 6
|
|
435
|
+
main_w, main_h = max(20, width - main_x - 2), max(8, height - 9)
|
|
436
|
+
_, box_y, box_width, _, _ = self._input_box_geometry(main_x, main_y, main_w, main_h, input_text)
|
|
437
|
+
label = "[ Paste ]"
|
|
438
|
+
paste_x = main_x + 2 + box_width - len("[ Copy prompt ]") - len(label) - 4
|
|
439
|
+
return paste_x <= x < paste_x + len(label) and y == box_y + 1
|
|
440
|
+
|
|
441
|
+
def _paste_into_input(self, input_text: str) -> str:
|
|
442
|
+
return input_text + self._paste_from_clipboard()
|
|
443
|
+
|
|
402
444
|
def _handle_thinking_click(self, x: int, y: int) -> bool:
|
|
403
445
|
height, width = curses.LINES, curses.COLS
|
|
404
446
|
sidebar_w = min(26, max(20, width // 4))
|
|
@@ -440,7 +482,16 @@ class CobaltTUI:
|
|
|
440
482
|
ch = stdscr.getch()
|
|
441
483
|
|
|
442
484
|
if ch == curses.KEY_MOUSE:
|
|
443
|
-
|
|
485
|
+
try:
|
|
486
|
+
_, x, y, _, button_state = curses.getmouse()
|
|
487
|
+
except curses.error:
|
|
488
|
+
continue
|
|
489
|
+
if button_state & getattr(curses, "BUTTON4_PRESSED", 0):
|
|
490
|
+
self.scroll_offset = min(self.scroll_offset + 3, max(0, len(self.history.messages) * 3))
|
|
491
|
+
continue
|
|
492
|
+
if button_state & getattr(curses, "BUTTON5_PRESSED", 0):
|
|
493
|
+
self.scroll_offset = max(0, self.scroll_offset - 3)
|
|
494
|
+
continue
|
|
444
495
|
copy_target = self._copy_button_at(x, y, input_text) if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED) else None
|
|
445
496
|
if copy_target:
|
|
446
497
|
try:
|
|
@@ -448,6 +499,13 @@ class CobaltTUI:
|
|
|
448
499
|
except (OSError, RuntimeError, ValueError) as exc:
|
|
449
500
|
self.history.add("assistant", f"Error: {exc}")
|
|
450
501
|
continue
|
|
502
|
+
if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED) and self._paste_button_at(x, y, input_text):
|
|
503
|
+
try:
|
|
504
|
+
input_text += self._paste_from_clipboard()
|
|
505
|
+
self.input_focused = True
|
|
506
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
507
|
+
self.history.add("assistant", f"Error: {exc}")
|
|
508
|
+
continue
|
|
451
509
|
button = self._handle_button_click(x, y, stdscr.getmaxyx()[1]) if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED) else None
|
|
452
510
|
if button in ("signin", "signup"):
|
|
453
511
|
self._open_auth(button)
|
|
@@ -482,6 +540,24 @@ class CobaltTUI:
|
|
|
482
540
|
continue
|
|
483
541
|
self.input_focused = False
|
|
484
542
|
continue
|
|
543
|
+
if ch == 22 and self.input_focused:
|
|
544
|
+
try:
|
|
545
|
+
input_text = self._paste_into_input(input_text)
|
|
546
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
547
|
+
self.history.add("assistant", f"Error: {exc}")
|
|
548
|
+
continue
|
|
549
|
+
if ch == curses.KEY_UP:
|
|
550
|
+
self.scroll_offset = min(self.scroll_offset + 1, max(0, len(self.history.messages) * 3))
|
|
551
|
+
continue
|
|
552
|
+
if ch == curses.KEY_DOWN:
|
|
553
|
+
self.scroll_offset = max(0, self.scroll_offset - 1)
|
|
554
|
+
continue
|
|
555
|
+
if ch == curses.KEY_PPAGE:
|
|
556
|
+
self.scroll_offset = min(self.scroll_offset + 8, max(0, len(self.history.messages) * 3))
|
|
557
|
+
continue
|
|
558
|
+
if ch == curses.KEY_NPAGE:
|
|
559
|
+
self.scroll_offset = max(0, self.scroll_offset - 8)
|
|
560
|
+
continue
|
|
485
561
|
if self.auth_view:
|
|
486
562
|
if ch in (10, 13, curses.KEY_ENTER):
|
|
487
563
|
if self.auth_field == 0:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import subprocess
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -73,6 +74,20 @@ def test_tui_copy_uses_available_clipboard_command(monkeypatch) -> None:
|
|
|
73
74
|
assert captured["kwargs"]["input"] == "prompt text"
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
def test_tui_copy_falls_back_when_xsel_fails(monkeypatch) -> None:
|
|
78
|
+
tui = CobaltTUI(settings=Settings())
|
|
79
|
+
|
|
80
|
+
monkeypatch.setattr("cobalt_cli_linux.tui.shutil.which", lambda command: f"/usr/bin/{command}")
|
|
81
|
+
|
|
82
|
+
def run(args, **kwargs):
|
|
83
|
+
if not args[0].endswith("xsel"):
|
|
84
|
+
raise subprocess.CalledProcessError(1, args)
|
|
85
|
+
|
|
86
|
+
monkeypatch.setattr("cobalt_cli_linux.tui.subprocess.run", run)
|
|
87
|
+
|
|
88
|
+
assert tui._copy_to_clipboard("text") == "Copied"
|
|
89
|
+
|
|
90
|
+
|
|
76
91
|
def test_streaming_empty_answer_gets_visible_fallback(monkeypatch, tmp_path: Path) -> None:
|
|
77
92
|
agent = CobaltAgent(settings=Settings(), history=ConversationHistory(tmp_path / "history.json"))
|
|
78
93
|
agent.profile_store.data["active"] = "personal"
|
|
@@ -247,3 +262,34 @@ def test_groq_client_auto_detects_available_model(monkeypatch) -> None:
|
|
|
247
262
|
client = GroqClient("test-key", model="auto")
|
|
248
263
|
assert client.resolve_model() == "llama-3.3-70b-versatile"
|
|
249
264
|
assert client.chat_completion([{"role": "user", "content": "hello"}]) == "ok"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_groq_client_clamps_max_tokens_to_model_limit(monkeypatch) -> None:
|
|
268
|
+
captured = {}
|
|
269
|
+
|
|
270
|
+
class FakeResponse:
|
|
271
|
+
status_code = 200
|
|
272
|
+
|
|
273
|
+
def json(self):
|
|
274
|
+
return {"choices": [{"message": {"content": "ok"}}]}
|
|
275
|
+
|
|
276
|
+
class FakeClient:
|
|
277
|
+
def __init__(self, timeout=None):
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
def __enter__(self):
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def __exit__(self, exc_type, exc, tb):
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def post(self, url, headers=None, json=None):
|
|
287
|
+
captured.update(json)
|
|
288
|
+
return FakeResponse()
|
|
289
|
+
|
|
290
|
+
monkeypatch.setattr("cobalt_cli_linux.groq_client.httpx.Client", FakeClient)
|
|
291
|
+
client = GroqClient("test-key", model="llama-3.1-8b-instant")
|
|
292
|
+
|
|
293
|
+
client.chat_completion([{"role": "user", "content": "hello"}], max_tokens=1024)
|
|
294
|
+
|
|
295
|
+
assert captured["max_tokens"] == 512
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/requires.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.6 → cobalt_cli_linux-0.1.7}/src/cobalt_cli_linux.egg-info/top_level.txt
RENAMED
|
File without changes
|