cobalt-cli-linux 0.1.5__tar.gz → 0.1.6__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.5 → cobalt_cli_linux-0.1.6}/PKG-INFO +1 -1
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/pyproject.toml +1 -1
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/agent.py +3 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/tui.py +54 -1
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/PKG-INFO +1 -1
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/tests/test_cli.py +25 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/README.md +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/setup.cfg +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/ASCII.txt +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/__init__.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/__main__.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/cli.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/config.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/deepseek_client.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/executor.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/groq_client.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/history.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/profiles.py +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/SOURCES.txt +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/dependency_links.txt +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/entry_points.txt +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/requires.txt +0 -0
- {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/top_level.txt +0 -0
|
@@ -169,6 +169,9 @@ class CobaltAgent:
|
|
|
169
169
|
on_chunk(chunk)
|
|
170
170
|
buffer = follow_up_buffer
|
|
171
171
|
|
|
172
|
+
if not buffer.strip():
|
|
173
|
+
buffer = "I could not produce a visible response for that request. Please try again."
|
|
174
|
+
|
|
172
175
|
if self.history.messages and self.history.messages[-1].get("role") == "assistant":
|
|
173
176
|
self.history.messages[-1]["content"] = buffer
|
|
174
177
|
if thinking_buffer:
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import curses
|
|
4
4
|
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
5
7
|
import textwrap
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
|
|
@@ -141,7 +143,7 @@ class CobaltTUI:
|
|
|
141
143
|
|
|
142
144
|
def _input_box_geometry(self, main_x: int, main_y: int, main_w: int, main_h: int, input_text: str) -> tuple[int, int, int, int, list[str]]:
|
|
143
145
|
box_width = max(20, main_w - 4)
|
|
144
|
-
text_width = max(10, box_width - 6)
|
|
146
|
+
text_width = max(10, box_width - 6 - len("[ Copy prompt ]") - 2)
|
|
145
147
|
input_lines = textwrap.wrap(
|
|
146
148
|
input_text,
|
|
147
149
|
width=text_width,
|
|
@@ -167,8 +169,33 @@ class CobaltTUI:
|
|
|
167
169
|
for index, line in enumerate(input_lines):
|
|
168
170
|
prompt = "> " if index == 0 else " "
|
|
169
171
|
stdscr.addstr(box_y + 1 + index, box_x, f"{prompt}{line}"[:box_width])
|
|
172
|
+
copy_label = "[ Copy prompt ]"
|
|
173
|
+
copy_x = max(box_x + 4, box_x + box_width - len(copy_label) - 2)
|
|
174
|
+
stdscr.addstr(box_y + 1, copy_x, copy_label[: max(0, box_width - (copy_x - box_x))], curses.A_DIM)
|
|
170
175
|
stdscr.addstr(box_y + box_height - 1, box_x, bottom[:box_width], curses.A_BOLD if focused else curses.A_DIM)
|
|
171
176
|
|
|
177
|
+
def _copy_to_clipboard(self, text: str) -> str:
|
|
178
|
+
if not text:
|
|
179
|
+
raise ValueError("Nothing to copy")
|
|
180
|
+
for command in ("wl-copy", "xclip", "xsel"):
|
|
181
|
+
executable = shutil.which(command)
|
|
182
|
+
if not executable:
|
|
183
|
+
continue
|
|
184
|
+
args = [executable]
|
|
185
|
+
if command == "xclip":
|
|
186
|
+
args.extend(["-selection", "clipboard"])
|
|
187
|
+
elif command == "xsel":
|
|
188
|
+
args.extend(["--clipboard", "--input"])
|
|
189
|
+
subprocess.run(args, input=text, text=True, check=True)
|
|
190
|
+
return "Copied"
|
|
191
|
+
raise RuntimeError("No clipboard utility found (install wl-clipboard, xclip, or xsel)")
|
|
192
|
+
|
|
193
|
+
def _latest_response(self) -> str:
|
|
194
|
+
for message in reversed(self.history.messages):
|
|
195
|
+
if message.get("role") == "assistant":
|
|
196
|
+
return message.get("content", "")
|
|
197
|
+
return ""
|
|
198
|
+
|
|
172
199
|
def _compose_lines(self, text: str, width: int) -> list[str]:
|
|
173
200
|
lines: list[str] = []
|
|
174
201
|
for paragraph in text.splitlines() or [""]:
|
|
@@ -286,6 +313,10 @@ class CobaltTUI:
|
|
|
286
313
|
clipped = line[: max(0, main_w - 4)]
|
|
287
314
|
stdscr.addstr(y, main_x + 2, clipped, attr)
|
|
288
315
|
|
|
316
|
+
copy_label = "[ Copy response ]"
|
|
317
|
+
copy_y = min(box_y - 1, main_y + main_h - box_height - 2)
|
|
318
|
+
stdscr.addstr(copy_y, main_x + 2, copy_label, curses.A_DIM)
|
|
319
|
+
|
|
289
320
|
self._draw_input_box(stdscr, box_x, box_y, box_width, box_height, input_lines, self.input_focused)
|
|
290
321
|
|
|
291
322
|
if status:
|
|
@@ -353,6 +384,21 @@ class CobaltTUI:
|
|
|
353
384
|
box_x, box_y, box_width, box_height, _ = self._input_box_geometry(main_x, main_y, main_w, main_h, input_text)
|
|
354
385
|
return box_x <= x < box_x + box_width and box_y <= y < box_y + box_height
|
|
355
386
|
|
|
387
|
+
def _copy_button_at(self, x: int, y: int, input_text: str) -> str | None:
|
|
388
|
+
height, width = curses.LINES, curses.COLS
|
|
389
|
+
sidebar_w = min(26, max(20, width // 4))
|
|
390
|
+
main_x, main_y = 3 + sidebar_w, 6
|
|
391
|
+
main_w, main_h = max(20, width - main_x - 2), max(8, height - 9)
|
|
392
|
+
_, box_y, box_width, box_height, _ = self._input_box_geometry(main_x, main_y, main_w, main_h, input_text)
|
|
393
|
+
prompt_copy_x = main_x + 2 + box_width - len("[ Copy prompt ]") - 2
|
|
394
|
+
if prompt_copy_x <= x < prompt_copy_x + len("[ Copy prompt ]") and box_y + 1 <= y <= box_y + 1:
|
|
395
|
+
return "prompt"
|
|
396
|
+
if self.history.messages:
|
|
397
|
+
copy_y = min(box_y - 1, main_y + main_h - box_height - 2)
|
|
398
|
+
if main_x + 2 <= x < main_x + 2 + len("[ Copy response ]") and y == copy_y:
|
|
399
|
+
return "response"
|
|
400
|
+
return None
|
|
401
|
+
|
|
356
402
|
def _handle_thinking_click(self, x: int, y: int) -> bool:
|
|
357
403
|
height, width = curses.LINES, curses.COLS
|
|
358
404
|
sidebar_w = min(26, max(20, width // 4))
|
|
@@ -395,6 +441,13 @@ class CobaltTUI:
|
|
|
395
441
|
|
|
396
442
|
if ch == curses.KEY_MOUSE:
|
|
397
443
|
_, x, y, _, button_state = curses.getmouse()
|
|
444
|
+
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
|
+
if copy_target:
|
|
446
|
+
try:
|
|
447
|
+
self._copy_to_clipboard(self._latest_response() if copy_target == "response" else input_text)
|
|
448
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
449
|
+
self.history.add("assistant", f"Error: {exc}")
|
|
450
|
+
continue
|
|
398
451
|
button = self._handle_button_click(x, y, stdscr.getmaxyx()[1]) if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED) else None
|
|
399
452
|
if button in ("signin", "signup"):
|
|
400
453
|
self._open_auth(button)
|
|
@@ -60,6 +60,31 @@ def test_tui_buttons_replace_chat_shortcuts() -> None:
|
|
|
60
60
|
assert tui._handle_button_click(buttons["delete"][0], buttons["delete"][1], 120) == "delete"
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def test_tui_copy_uses_available_clipboard_command(monkeypatch) -> None:
|
|
64
|
+
tui = CobaltTUI(settings=Settings())
|
|
65
|
+
tui.history.add("assistant", "response text")
|
|
66
|
+
captured = {}
|
|
67
|
+
|
|
68
|
+
monkeypatch.setattr("cobalt_cli_linux.tui.shutil.which", lambda command: "/usr/bin/wl-copy" if command == "wl-copy" else None)
|
|
69
|
+
monkeypatch.setattr("cobalt_cli_linux.tui.subprocess.run", lambda args, **kwargs: captured.update(args=args, kwargs=kwargs))
|
|
70
|
+
|
|
71
|
+
assert tui._latest_response() == "response text"
|
|
72
|
+
assert tui._copy_to_clipboard("prompt text") == "Copied"
|
|
73
|
+
assert captured["kwargs"]["input"] == "prompt text"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_streaming_empty_answer_gets_visible_fallback(monkeypatch, tmp_path: Path) -> None:
|
|
77
|
+
agent = CobaltAgent(settings=Settings(), history=ConversationHistory(tmp_path / "history.json"))
|
|
78
|
+
agent.profile_store.data["active"] = "personal"
|
|
79
|
+
|
|
80
|
+
monkeypatch.setattr(agent.client, "chat_completion_stream_events", lambda *args, **kwargs: iter(()))
|
|
81
|
+
|
|
82
|
+
response = agent.process_stream("hello")
|
|
83
|
+
|
|
84
|
+
assert response.startswith("I could not produce a visible response")
|
|
85
|
+
assert agent.history.messages[-1]["content"] == response
|
|
86
|
+
|
|
87
|
+
|
|
63
88
|
def test_no_prompt_launches_tui(monkeypatch) -> None:
|
|
64
89
|
launched = []
|
|
65
90
|
|
|
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.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/requires.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/top_level.txt
RENAMED
|
File without changes
|