cobalt-cli-linux 0.1.1__tar.gz → 0.1.2__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 (22) hide show
  1. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/PKG-INFO +3 -1
  2. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/README.md +2 -0
  3. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/pyproject.toml +1 -1
  4. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/tui.py +65 -18
  5. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux.egg-info/PKG-INFO +3 -1
  6. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/tests/test_cli.py +4 -2
  7. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/setup.cfg +0 -0
  8. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/ASCII.txt +0 -0
  9. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/__init__.py +0 -0
  10. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/__main__.py +0 -0
  11. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/agent.py +0 -0
  12. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/cli.py +0 -0
  13. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/config.py +0 -0
  14. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/deepseek_client.py +0 -0
  15. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/executor.py +0 -0
  16. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/groq_client.py +0 -0
  17. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux/history.py +0 -0
  18. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux.egg-info/SOURCES.txt +0 -0
  19. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux.egg-info/dependency_links.txt +0 -0
  20. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux.egg-info/entry_points.txt +0 -0
  21. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/src/cobalt_cli_linux.egg-info/requires.txt +0 -0
  22. {cobalt_cli_linux-0.1.1 → cobalt_cli_linux-0.1.2}/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.1
3
+ Version: 0.1.2
4
4
  Summary: Agentic Groq-powered CLI assistant for Debian Linux
5
5
  Author: Cobalt
6
6
  License-Expression: MIT
@@ -77,6 +77,8 @@ cobalt --interactive
77
77
 
78
78
  This launches a fullscreen TUI that feels similar to a professional agent console, with the message stream and a compact prompt area instead of a raw shell prompt.
79
79
 
80
+ In the interface, press `Escape` to unfocus the prompt. While unfocused, `n` starts a new chat and `d` deletes the selected chat; click the prompt or type a character to focus it again. Long prompts wrap inside the prompt box.
81
+
80
82
  You can also pass arguments directly:
81
83
 
82
84
  ```bash
@@ -52,6 +52,8 @@ cobalt --interactive
52
52
 
53
53
  This launches a fullscreen TUI that feels similar to a professional agent console, with the message stream and a compact prompt area instead of a raw shell prompt.
54
54
 
55
+ In the interface, press `Escape` to unfocus the prompt. While unfocused, `n` starts a new chat and `d` deletes the selected chat; click the prompt or type a character to focus it again. Long prompts wrap inside the prompt box.
56
+
55
57
  You can also pass arguments directly:
56
58
 
57
59
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cobalt-cli-linux"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Agentic Groq-powered CLI assistant for Debian Linux"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -23,6 +23,7 @@ class CobaltTUI:
23
23
  self.history = self.current_chat
24
24
  self.saved_chats = self.chat_files
25
25
  self.ascii_lines = self._load_ascii_art()
26
+ self.input_focused = True
26
27
 
27
28
  def _load_ascii_art(self) -> list[str]:
28
29
  ascii_path = Path(__file__).with_name("ASCII.txt")
@@ -58,8 +59,38 @@ class CobaltTUI:
58
59
  title = " ".join(words[:4]).strip()
59
60
  return title.title() if title else "New chat"
60
61
 
61
- def _should_handle_shortcut(self, ch: int) -> bool:
62
- return ch in (14, 4)
62
+ def _should_handle_shortcut(self, ch: int, input_focused: bool = False) -> bool:
63
+ return not input_focused and ch in (14, 4, ord("n"), ord("d"))
64
+
65
+ 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]]:
66
+ box_width = max(20, main_w - 4)
67
+ text_width = max(10, box_width - 6)
68
+ input_lines = textwrap.wrap(
69
+ input_text,
70
+ width=text_width,
71
+ replace_whitespace=False,
72
+ drop_whitespace=False,
73
+ break_long_words=True,
74
+ break_on_hyphens=False,
75
+ ) or [""]
76
+ max_lines = max(1, min(6, main_h - 6))
77
+ input_lines = input_lines[-max_lines:]
78
+ box_height = len(input_lines) + 3
79
+ box_x = main_x + 2
80
+ box_y = main_y + main_h - box_height - 1
81
+ return box_x, box_y, box_width, box_height, input_lines
82
+
83
+ def _draw_input_box(self, stdscr, box_x: int, box_y: int, box_width: int, box_height: int, input_lines: list[str], focused: bool) -> None:
84
+ rule = "-" * max(0, box_width - 2)
85
+ top_label = " Ask AI "
86
+ top = f"-{top_label}{rule}"
87
+ bottom_label = " [Enter to submit] "
88
+ bottom = f"-{'-' * max(0, box_width - len(bottom_label) - 2)}{bottom_label}-"
89
+ stdscr.addstr(box_y, box_x, top[:box_width])
90
+ for index, line in enumerate(input_lines):
91
+ prompt = "> " if index == 0 else " "
92
+ stdscr.addstr(box_y + 1 + index, box_x, f"{prompt}{line}"[:box_width])
93
+ stdscr.addstr(box_y + box_height - 1, box_x, bottom[:box_width], curses.A_BOLD if focused else curses.A_DIM)
63
94
 
64
95
  def _compose_lines(self, text: str, width: int) -> list[str]:
65
96
  lines: list[str] = []
@@ -116,15 +147,15 @@ class CobaltTUI:
116
147
  main_h = max(8, height - 4)
117
148
  self._draw_box(stdscr, main_y, main_x, main_h, main_w, f" {self.history.title or 'conversation'} ")
118
149
 
150
+ box_x, box_y, box_width, box_height, input_lines = self._input_box_geometry(main_x, main_y, main_w, main_h, input_text)
119
151
  if not self.history.messages:
152
+ content_height = max(1, box_y - main_y - 1)
153
+ art_start = main_y + max(1, (content_height - len(self.ascii_lines)) // 2)
120
154
  for idx, line in enumerate(self.ascii_lines):
121
- y = main_y + 2 + idx
155
+ y = art_start + idx
122
156
  x = main_x + max(0, (main_w - len(line)) // 2)
123
- stdscr.addstr(y, x, line[: max(0, main_w - 2)])
124
- prompt = f"> {input_text}" if input_text else "> "
125
- prompt_y = main_y + 2 + len(self.ascii_lines) + 2
126
- prompt_x = main_x + max(0, (main_w - len(prompt)) // 2)
127
- stdscr.addstr(prompt_y, prompt_x, prompt[: max(0, main_w - 2)], curses.A_REVERSE)
157
+ if y < box_y:
158
+ stdscr.addstr(y, x, line[: max(0, main_w - 2)])
128
159
  else:
129
160
  rows: list[str] = []
130
161
  for message in self.history.latest(18):
@@ -135,16 +166,15 @@ class CobaltTUI:
135
166
  rows.append(f"{prefix}{line}" if prefix and line else line)
136
167
  prefix = ""
137
168
 
138
- visible = rows[-max(4, main_h - 4):]
169
+ visible = rows[-max(1, box_y - main_y - 2):]
139
170
  for idx, line in enumerate(visible[: main_h - 4]):
140
171
  y = main_y + 1 + idx
172
+ if y >= box_y:
173
+ break
141
174
  clipped = line[: max(0, main_w - 4)]
142
175
  stdscr.addstr(y, main_x + 2, clipped)
143
176
 
144
- prompt = f"> {input_text}" if input_text else "> "
145
- prompt_y = main_y + main_h - 3
146
- prompt_x = main_x + max(0, (main_w - len(prompt)) // 2)
147
- stdscr.addstr(prompt_y, prompt_x, prompt[: max(0, main_w - 2)], curses.A_REVERSE)
177
+ self._draw_input_box(stdscr, box_x, box_y, box_width, box_height, input_lines, self.input_focused)
148
178
 
149
179
  if status:
150
180
  status_line = status[: max(0, width - 1)]
@@ -190,6 +220,16 @@ class CobaltTUI:
190
220
  return True
191
221
  return False
192
222
 
223
+ def _click_is_in_input(self, x: int, y: int, input_text: str) -> bool:
224
+ height, width = curses.LINES, curses.COLS
225
+ sidebar_w = min(26, max(20, width // 4))
226
+ main_x = 3 + sidebar_w
227
+ main_y = 1
228
+ main_w = max(20, width - main_x - 2)
229
+ main_h = max(8, height - 4)
230
+ box_x, box_y, box_width, box_height, _ = self._input_box_geometry(main_x, main_y, main_w, main_h, input_text)
231
+ return box_x <= x < box_x + box_width and box_y <= y < box_y + box_height
232
+
193
233
  def run(self) -> int:
194
234
  def _main(stdscr):
195
235
  curses.curs_set(0)
@@ -202,6 +242,7 @@ class CobaltTUI:
202
242
  curses.noecho()
203
243
  curses.mousemask(curses.BUTTON1_PRESSED | curses.BUTTON1_RELEASED | curses.BUTTON1_CLICKED)
204
244
  input_text = ""
245
+ self.input_focused = True
205
246
  status = ""
206
247
 
207
248
  while True:
@@ -210,15 +251,22 @@ class CobaltTUI:
210
251
 
211
252
  if ch == curses.KEY_MOUSE:
212
253
  _, x, y, _, button_state = curses.getmouse()
254
+ if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED) and self._click_is_in_input(x, y, input_text):
255
+ self.input_focused = True
256
+ continue
213
257
  if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED) and self._handle_mouse_click(x, y):
214
258
  status = "Chat selected"
215
259
  input_text = ""
260
+ self.input_focused = False
216
261
  continue
217
262
 
218
263
  if ch == 3:
219
264
  break
220
- if self._should_handle_shortcut(ch):
221
- if ch == 14:
265
+ if ch == 27:
266
+ self.input_focused = False
267
+ continue
268
+ if self._should_handle_shortcut(ch, self.input_focused):
269
+ if ch in (14, ord("n")):
222
270
  new_chat = self.chat_store.new_chat(title="New chat")
223
271
  self.current_chat = new_chat
224
272
  self.history = self.current_chat
@@ -228,7 +276,7 @@ class CobaltTUI:
228
276
  status = "New chat started"
229
277
  continue
230
278
 
231
- if ch == 4 and self.selected_chat_path:
279
+ if ch in (4, ord("d")) and self.selected_chat_path:
232
280
  target = Path(self.selected_chat_path)
233
281
  if target.exists():
234
282
  self.chat_store.delete_chat(target)
@@ -263,9 +311,8 @@ class CobaltTUI:
263
311
  if ch in (curses.KEY_RESIZE,):
264
312
  continue
265
313
  if 32 <= ch <= 126:
314
+ self.input_focused = True
266
315
  input_text += chr(ch)
267
- elif ch == 27:
268
- break
269
316
 
270
317
  return 0
271
318
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cobalt-cli-linux
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Agentic Groq-powered CLI assistant for Debian Linux
5
5
  Author: Cobalt
6
6
  License-Expression: MIT
@@ -77,6 +77,8 @@ cobalt --interactive
77
77
 
78
78
  This launches a fullscreen TUI that feels similar to a professional agent console, with the message stream and a compact prompt area instead of a raw shell prompt.
79
79
 
80
+ In the interface, press `Escape` to unfocus the prompt. While unfocused, `n` starts a new chat and `d` deletes the selected chat; click the prompt or type a character to focus it again. Long prompts wrap inside the prompt box.
81
+
80
82
  You can also pass arguments directly:
81
83
 
82
84
  ```bash
@@ -51,9 +51,11 @@ def test_tui_shortcuts_ignore_typed_letters() -> None:
51
51
 
52
52
  assert tui._should_handle_shortcut(14) is True
53
53
  assert tui._should_handle_shortcut(4) is True
54
+ assert tui._should_handle_shortcut(ord("n")) is True
55
+ assert tui._should_handle_shortcut(ord("d")) is True
54
56
  assert tui._should_handle_shortcut(ord("q")) is False
55
- assert tui._should_handle_shortcut(ord("n")) is False
56
- assert tui._should_handle_shortcut(ord("d")) is False
57
+ assert tui._should_handle_shortcut(ord("n"), True) is False
58
+ assert tui._should_handle_shortcut(ord("d"), True) is False
57
59
 
58
60
 
59
61
  def test_q_is_not_a_shortcut_when_typing() -> None: