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.
Files changed (23) hide show
  1. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/PKG-INFO +1 -1
  2. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/pyproject.toml +1 -1
  3. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/agent.py +3 -0
  4. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/tui.py +54 -1
  5. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/PKG-INFO +1 -1
  6. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/tests/test_cli.py +25 -0
  7. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/README.md +0 -0
  8. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/setup.cfg +0 -0
  9. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/ASCII.txt +0 -0
  10. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/__init__.py +0 -0
  11. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/__main__.py +0 -0
  12. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/cli.py +0 -0
  13. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/config.py +0 -0
  14. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/deepseek_client.py +0 -0
  15. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/executor.py +0 -0
  16. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/groq_client.py +0 -0
  17. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/history.py +0 -0
  18. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/profiles.py +0 -0
  19. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/SOURCES.txt +0 -0
  20. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/dependency_links.txt +0 -0
  21. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/entry_points.txt +0 -0
  22. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/requires.txt +0 -0
  23. {cobalt_cli_linux-0.1.5 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cobalt-cli-linux
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Agentic Groq-powered CLI assistant for Debian Linux
5
5
  Author: Cobalt
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cobalt-cli-linux"
7
- version = "0.1.5"
7
+ version = "0.1.6"
8
8
  description = "Agentic Groq-powered CLI assistant for Debian Linux"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cobalt-cli-linux
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Agentic Groq-powered CLI assistant for Debian Linux
5
5
  Author: Cobalt
6
6
  License-Expression: MIT
@@ -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