cobalt-cli-linux 0.1.4__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.4 → cobalt_cli_linux-0.1.6}/PKG-INFO +3 -1
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/README.md +2 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/pyproject.toml +1 -1
- cobalt_cli_linux-0.1.6/src/cobalt_cli_linux/ASCII.txt +5 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/agent.py +3 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/profiles.py +25 -2
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/tui.py +122 -2
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/PKG-INFO +3 -1
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/tests/test_cli.py +36 -0
- cobalt_cli_linux-0.1.4/src/cobalt_cli_linux/ASCII.txt +0 -5
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/setup.cfg +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/__init__.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/__main__.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/cli.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/config.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/deepseek_client.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/executor.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/groq_client.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux/history.py +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/SOURCES.txt +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/dependency_links.txt +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/entry_points.txt +0 -0
- {cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/requires.txt +0 -0
- {cobalt_cli_linux-0.1.4 → 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.
|
|
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
|
|
@@ -81,6 +81,8 @@ In the interface, click the `New chat` and `Delete chat` buttons to manage conve
|
|
|
81
81
|
|
|
82
82
|
Assistant reasoning is stored separately and can be expanded by clicking its `thinking` row. Markdown headings, lists, inline backticks, and fenced code are styled in the terminal. Guest profiles are limited to three prompts per hour.
|
|
83
83
|
|
|
84
|
+
The interface includes `Sign in` and `Sign up` buttons for local profiles. Profiles are stored locally; guest mode remains available with the prompt limit.
|
|
85
|
+
|
|
84
86
|
You can also pass arguments directly:
|
|
85
87
|
|
|
86
88
|
```bash
|
|
@@ -56,6 +56,8 @@ In the interface, click the `New chat` and `Delete chat` buttons to manage conve
|
|
|
56
56
|
|
|
57
57
|
Assistant reasoning is stored separately and can be expanded by clicking its `thinking` row. Markdown headings, lists, inline backticks, and fenced code are styled in the terminal. Guest profiles are limited to three prompts per hour.
|
|
58
58
|
|
|
59
|
+
The interface includes `Sign in` and `Sign up` buttons for local profiles. Profiles are stored locally; guest mode remains available with the prompt limit.
|
|
60
|
+
|
|
59
61
|
You can also pass arguments directly:
|
|
60
62
|
|
|
61
63
|
```bash
|
|
@@ -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:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
from datetime import datetime, timedelta, timezone
|
|
5
6
|
from pathlib import Path
|
|
@@ -13,12 +14,13 @@ class ProfileStore:
|
|
|
13
14
|
|
|
14
15
|
def _load(self) -> dict:
|
|
15
16
|
if not self.path.exists():
|
|
16
|
-
return {"profiles": {"guest": {"prompts": []}}, "active": "guest"}
|
|
17
|
+
return {"profiles": {"guest": {"prompts": []}}, "accounts": {}, "active": "guest"}
|
|
17
18
|
try:
|
|
18
19
|
value = json.loads(self.path.read_text(encoding="utf-8"))
|
|
19
20
|
except json.JSONDecodeError:
|
|
20
|
-
return {"profiles": {"guest": {"prompts": []}}, "active": "guest"}
|
|
21
|
+
return {"profiles": {"guest": {"prompts": []}}, "accounts": {}, "active": "guest"}
|
|
21
22
|
value.setdefault("profiles", {}).setdefault("guest", {"prompts": []})
|
|
23
|
+
value.setdefault("accounts", {})
|
|
22
24
|
value.setdefault("active", "guest")
|
|
23
25
|
return value
|
|
24
26
|
|
|
@@ -38,6 +40,27 @@ class ProfileStore:
|
|
|
38
40
|
self.save()
|
|
39
41
|
return name
|
|
40
42
|
|
|
43
|
+
def sign_up(self, name: str, password: str) -> str:
|
|
44
|
+
name = " ".join(name.strip().split())
|
|
45
|
+
if not name or not password:
|
|
46
|
+
raise ValueError("Name and password are required")
|
|
47
|
+
if name in self.data["accounts"]:
|
|
48
|
+
raise ValueError("Profile already exists")
|
|
49
|
+
digest = hashlib.sha256(password.encode("utf-8")).hexdigest()
|
|
50
|
+
self.data["accounts"][name] = digest
|
|
51
|
+
self.data["profiles"].setdefault(name, {"prompts": []})
|
|
52
|
+
self.data["active"] = name
|
|
53
|
+
self.save()
|
|
54
|
+
return name
|
|
55
|
+
|
|
56
|
+
def sign_in(self, name: str, password: str) -> str:
|
|
57
|
+
digest = hashlib.sha256(password.encode("utf-8")).hexdigest()
|
|
58
|
+
if self.data["accounts"].get(name) != digest:
|
|
59
|
+
raise ValueError("Invalid profile name or password")
|
|
60
|
+
self.data["active"] = name
|
|
61
|
+
self.save()
|
|
62
|
+
return name
|
|
63
|
+
|
|
41
64
|
def delete(self, name: str | None = None) -> bool:
|
|
42
65
|
name = name or self.active
|
|
43
66
|
if name == "guest" or name not in self.data["profiles"]:
|
|
@@ -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
|
|
|
@@ -27,6 +29,9 @@ class CobaltTUI:
|
|
|
27
29
|
self.ascii_lines = self._load_ascii_art()
|
|
28
30
|
self.thinking_expanded: set[int] = set()
|
|
29
31
|
self.profile_store = ProfileStore()
|
|
32
|
+
self.auth_view: str | None = None
|
|
33
|
+
self.auth_field = 0
|
|
34
|
+
self.auth_values = ["", ""]
|
|
30
35
|
|
|
31
36
|
def _load_ascii_art(self) -> list[str]:
|
|
32
37
|
ascii_path = Path(__file__).resolve().parents[2] / "ASCII.txt"
|
|
@@ -65,9 +70,15 @@ class CobaltTUI:
|
|
|
65
70
|
new_label = "[ New chat ]"
|
|
66
71
|
delete_x = max(1, width - len(delete_label) - 2)
|
|
67
72
|
new_x = max(1, delete_x - len(new_label) - 2)
|
|
73
|
+
signup_label = "[ Sign up ]"
|
|
74
|
+
signin_label = "[ Sign in ]"
|
|
75
|
+
signin_x = max(1, new_x - len(signin_label) - 2)
|
|
76
|
+
signup_x = max(1, signin_x - len(signup_label) - 2)
|
|
68
77
|
return {
|
|
69
78
|
"new": (new_x, 0, len(new_label), 1),
|
|
70
79
|
"delete": (delete_x, 0, len(delete_label), 1),
|
|
80
|
+
"signin": (signin_x, 0, len(signin_label), 1),
|
|
81
|
+
"signup": (signup_x, 0, len(signup_label), 1),
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
def _handle_button_click(self, x: int, y: int, width: int | None = None) -> str | None:
|
|
@@ -98,9 +109,41 @@ class CobaltTUI:
|
|
|
98
109
|
else:
|
|
99
110
|
self._start_new_chat()
|
|
100
111
|
|
|
112
|
+
def _open_auth(self, view: str) -> None:
|
|
113
|
+
self.auth_view = view
|
|
114
|
+
self.auth_field = 0
|
|
115
|
+
self.auth_values = ["", ""]
|
|
116
|
+
self.input_focused = True
|
|
117
|
+
|
|
118
|
+
def _submit_auth(self) -> None:
|
|
119
|
+
try:
|
|
120
|
+
if self.auth_view == "signup":
|
|
121
|
+
self.profile_store.sign_up(*self.auth_values)
|
|
122
|
+
elif self.auth_view == "signin":
|
|
123
|
+
self.profile_store.sign_in(*self.auth_values)
|
|
124
|
+
self.auth_view = None
|
|
125
|
+
self.auth_values = ["", ""]
|
|
126
|
+
except (ValueError, OSError) as exc:
|
|
127
|
+
self.auth_values[1] = str(exc)
|
|
128
|
+
|
|
129
|
+
def _render_auth(self, stdscr, height: int, width: int) -> None:
|
|
130
|
+
title = "Create profile" if self.auth_view == "signup" else "Sign in"
|
|
131
|
+
fields = ("Profile name", "Password")
|
|
132
|
+
panel_width = min(64, max(36, width - 8))
|
|
133
|
+
panel_x = max(2, (width - panel_width) // 2)
|
|
134
|
+
panel_y = max(3, (height - 12) // 2)
|
|
135
|
+
self._draw_box(stdscr, panel_y, panel_x, 10, panel_width, f" {title} ")
|
|
136
|
+
for index, label in enumerate(fields):
|
|
137
|
+
value = self.auth_values[index]
|
|
138
|
+
if index == 1 and value and not value.startswith("Invalid"):
|
|
139
|
+
value = "*" * len(value)
|
|
140
|
+
marker = "> " if index == self.auth_field else " "
|
|
141
|
+
stdscr.addstr(panel_y + 2 + index * 2, panel_x + 3, f"{marker}{label}: {value}"[: panel_width - 6])
|
|
142
|
+
stdscr.addstr(panel_y + 7, panel_x + 3, "Enter: next/submit | Escape: back"[: panel_width - 6], curses.A_DIM)
|
|
143
|
+
|
|
101
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]]:
|
|
102
145
|
box_width = max(20, main_w - 4)
|
|
103
|
-
text_width = max(10, box_width - 6)
|
|
146
|
+
text_width = max(10, box_width - 6 - len("[ Copy prompt ]") - 2)
|
|
104
147
|
input_lines = textwrap.wrap(
|
|
105
148
|
input_text,
|
|
106
149
|
width=text_width,
|
|
@@ -126,8 +169,33 @@ class CobaltTUI:
|
|
|
126
169
|
for index, line in enumerate(input_lines):
|
|
127
170
|
prompt = "> " if index == 0 else " "
|
|
128
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)
|
|
129
175
|
stdscr.addstr(box_y + box_height - 1, box_x, bottom[:box_width], curses.A_BOLD if focused else curses.A_DIM)
|
|
130
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
|
+
|
|
131
199
|
def _compose_lines(self, text: str, width: int) -> list[str]:
|
|
132
200
|
lines: list[str] = []
|
|
133
201
|
for paragraph in text.splitlines() or [""]:
|
|
@@ -188,9 +256,15 @@ class CobaltTUI:
|
|
|
188
256
|
header = " Cobalt Code | terminal agent "
|
|
189
257
|
stdscr.addstr(0, 1, header[: max(0, width - 2)], curses.A_BOLD)
|
|
190
258
|
for name, (button_x, button_y, button_width, _) in self._button_regions(width).items():
|
|
191
|
-
|
|
259
|
+
labels = {"new": "[ New chat ]", "delete": "[ Delete chat ]", "signin": "[ Sign in ]", "signup": "[ Sign up ]"}
|
|
260
|
+
label = labels[name]
|
|
192
261
|
stdscr.addstr(button_y, button_x, label[:button_width], curses.A_REVERSE if name == "new" else curses.A_NORMAL)
|
|
193
262
|
|
|
263
|
+
if self.auth_view:
|
|
264
|
+
self._render_auth(stdscr, height, width)
|
|
265
|
+
stdscr.refresh()
|
|
266
|
+
return
|
|
267
|
+
|
|
194
268
|
sidebar_w = min(26, max(20, width // 4))
|
|
195
269
|
sidebar_x = 1
|
|
196
270
|
sidebar_y = 6
|
|
@@ -239,6 +313,10 @@ class CobaltTUI:
|
|
|
239
313
|
clipped = line[: max(0, main_w - 4)]
|
|
240
314
|
stdscr.addstr(y, main_x + 2, clipped, attr)
|
|
241
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
|
+
|
|
242
320
|
self._draw_input_box(stdscr, box_x, box_y, box_width, box_height, input_lines, self.input_focused)
|
|
243
321
|
|
|
244
322
|
if status:
|
|
@@ -306,6 +384,21 @@ class CobaltTUI:
|
|
|
306
384
|
box_x, box_y, box_width, box_height, _ = self._input_box_geometry(main_x, main_y, main_w, main_h, input_text)
|
|
307
385
|
return box_x <= x < box_x + box_width and box_y <= y < box_y + box_height
|
|
308
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
|
+
|
|
309
402
|
def _handle_thinking_click(self, x: int, y: int) -> bool:
|
|
310
403
|
height, width = curses.LINES, curses.COLS
|
|
311
404
|
sidebar_w = min(26, max(20, width // 4))
|
|
@@ -348,7 +441,17 @@ class CobaltTUI:
|
|
|
348
441
|
|
|
349
442
|
if ch == curses.KEY_MOUSE:
|
|
350
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
|
|
351
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
|
|
452
|
+
if button in ("signin", "signup"):
|
|
453
|
+
self._open_auth(button)
|
|
454
|
+
continue
|
|
352
455
|
if button == "new":
|
|
353
456
|
self._start_new_chat()
|
|
354
457
|
input_text = ""
|
|
@@ -373,8 +476,25 @@ class CobaltTUI:
|
|
|
373
476
|
if ch == 3:
|
|
374
477
|
break
|
|
375
478
|
if ch == 27:
|
|
479
|
+
if self.auth_view:
|
|
480
|
+
self.auth_view = None
|
|
481
|
+
self.auth_values = ["", ""]
|
|
482
|
+
continue
|
|
376
483
|
self.input_focused = False
|
|
377
484
|
continue
|
|
485
|
+
if self.auth_view:
|
|
486
|
+
if ch in (10, 13, curses.KEY_ENTER):
|
|
487
|
+
if self.auth_field == 0:
|
|
488
|
+
self.auth_field = 1
|
|
489
|
+
else:
|
|
490
|
+
self._submit_auth()
|
|
491
|
+
continue
|
|
492
|
+
if ch in (curses.KEY_BACKSPACE, 127, 8):
|
|
493
|
+
self.auth_values[self.auth_field] = self.auth_values[self.auth_field][:-1]
|
|
494
|
+
continue
|
|
495
|
+
if 32 <= ch <= 126:
|
|
496
|
+
self.auth_values[self.auth_field] += chr(ch)
|
|
497
|
+
continue
|
|
378
498
|
if ch in (10, 13, curses.KEY_ENTER):
|
|
379
499
|
if input_text.strip():
|
|
380
500
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cobalt-cli-linux
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
@@ -81,6 +81,8 @@ In the interface, click the `New chat` and `Delete chat` buttons to manage conve
|
|
|
81
81
|
|
|
82
82
|
Assistant reasoning is stored separately and can be expanded by clicking its `thinking` row. Markdown headings, lists, inline backticks, and fenced code are styled in the terminal. Guest profiles are limited to three prompts per hour.
|
|
83
83
|
|
|
84
|
+
The interface includes `Sign in` and `Sign up` buttons for local profiles. Profiles are stored locally; guest mode remains available with the prompt limit.
|
|
85
|
+
|
|
84
86
|
You can also pass arguments directly:
|
|
85
87
|
|
|
86
88
|
```bash
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
3
5
|
from cobalt_cli_linux import cli
|
|
4
6
|
from cobalt_cli_linux.agent import CobaltAgent
|
|
5
7
|
from cobalt_cli_linux.config import Settings
|
|
@@ -58,6 +60,31 @@ def test_tui_buttons_replace_chat_shortcuts() -> None:
|
|
|
58
60
|
assert tui._handle_button_click(buttons["delete"][0], buttons["delete"][1], 120) == "delete"
|
|
59
61
|
|
|
60
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
|
+
|
|
61
88
|
def test_no_prompt_launches_tui(monkeypatch) -> None:
|
|
62
89
|
launched = []
|
|
63
90
|
|
|
@@ -133,6 +160,15 @@ def test_profiles_create_delete_and_guest_limit(tmp_path: Path) -> None:
|
|
|
133
160
|
assert store.active == "guest"
|
|
134
161
|
|
|
135
162
|
|
|
163
|
+
def test_profiles_sign_up_and_sign_in(tmp_path: Path) -> None:
|
|
164
|
+
store = ProfileStore(tmp_path / "profiles.json")
|
|
165
|
+
|
|
166
|
+
assert store.sign_up("Ada", "secret") == "Ada"
|
|
167
|
+
assert store.sign_in("Ada", "secret") == "Ada"
|
|
168
|
+
with pytest.raises(ValueError):
|
|
169
|
+
store.sign_in("Ada", "wrong")
|
|
170
|
+
|
|
171
|
+
|
|
136
172
|
def test_tui_empty_chat_does_not_load_prompt_art_as_body_content(tmp_path: Path) -> None:
|
|
137
173
|
tui = CobaltTUI(settings=Settings())
|
|
138
174
|
|
|
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.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/requires.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.4 → cobalt_cli_linux-0.1.6}/src/cobalt_cli_linux.egg-info/top_level.txt
RENAMED
|
File without changes
|