kader 0.1.3__tar.gz → 0.1.5__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.
- {kader-0.1.3 → kader-0.1.5}/PKG-INFO +1 -1
- {kader-0.1.3 → kader-0.1.5}/cli/app.py +63 -25
- {kader-0.1.3 → kader-0.1.5}/cli/app.tcss +9 -10
- {kader-0.1.3 → kader-0.1.5}/cli/utils.py +13 -8
- {kader-0.1.3 → kader-0.1.5}/cli/widgets/confirmation.py +5 -5
- {kader-0.1.3 → kader-0.1.5}/cli/widgets/conversation.py +1 -1
- {kader-0.1.3 → kader-0.1.5}/cli/widgets/loading.py +1 -1
- {kader-0.1.3 → kader-0.1.5}/kader/agent/base.py +10 -5
- {kader-0.1.3 → kader-0.1.5}/pyproject.toml +1 -1
- {kader-0.1.3 → kader-0.1.5}/uv.lock +1 -1
- {kader-0.1.3 → kader-0.1.5}/.github/workflows/ci.yml +0 -0
- {kader-0.1.3 → kader-0.1.5}/.github/workflows/release.yml +0 -0
- {kader-0.1.3 → kader-0.1.5}/.gitignore +0 -0
- {kader-0.1.3 → kader-0.1.5}/.python-version +0 -0
- {kader-0.1.3 → kader-0.1.5}/.qwen/QWEN.md +0 -0
- {kader-0.1.3 → kader-0.1.5}/.qwen/agents/technical-writer.md +0 -0
- {kader-0.1.3 → kader-0.1.5}/.qwen/agents/test-automation-specialist.md +0 -0
- {kader-0.1.3 → kader-0.1.5}/README.md +0 -0
- {kader-0.1.3 → kader-0.1.5}/cli/README.md +0 -0
- {kader-0.1.3 → kader-0.1.5}/cli/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/cli/__main__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/cli/widgets/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/.gitignore +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/README.md +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/memory_example.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/ollama_example.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/planning_agent_example.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/python_developer/main.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/python_developer/template.yaml +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/react_agent_example.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/simple_agent.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/todo_agent/main.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/examples/tools_example.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/agent/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/agent/agents.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/agent/logger.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/config.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/memory/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/memory/conversation.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/memory/session.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/memory/state.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/memory/types.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/prompts/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/prompts/agent_prompts.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/prompts/base.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/prompts/templates/planning_agent.j2 +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/prompts/templates/react_agent.j2 +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/providers/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/providers/base.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/providers/mock.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/providers/ollama.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/README.md +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/__init__.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/base.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/exec_commands.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/filesys.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/filesystem.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/protocol.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/rag.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/todo.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/utils.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/kader/tools/web.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/conftest.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/providers/test_mock.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/providers/test_ollama.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/providers/test_providers_base.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/test_agent_logger.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/test_agent_logger_integration.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/test_base_agent.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/test_file_memory.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/test_todo_tool.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/tools/test_exec_commands.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/tools/test_filesys_tools.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/tools/test_filesystem_tools.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/tools/test_rag.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/tools/test_tools_base.py +0 -0
- {kader-0.1.3 → kader-0.1.5}/tests/tools/test_web.py +0 -0
|
@@ -10,12 +10,12 @@ from textual.app import App, ComposeResult
|
|
|
10
10
|
from textual.binding import Binding
|
|
11
11
|
from textual.containers import Container, Horizontal, Vertical
|
|
12
12
|
from textual.widgets import (
|
|
13
|
-
DirectoryTree,
|
|
14
13
|
Footer,
|
|
15
14
|
Header,
|
|
16
15
|
Input,
|
|
17
16
|
Markdown,
|
|
18
17
|
Static,
|
|
18
|
+
Tree,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
from kader.agent.agents import ReActAgent
|
|
@@ -66,11 +66,18 @@ MIN_WIDTH = 89
|
|
|
66
66
|
MIN_HEIGHT = 29
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
class ASCIITree(Tree):
|
|
70
|
+
"""A Tree widget that uses no icons."""
|
|
71
|
+
|
|
72
|
+
ICON_NODE = ""
|
|
73
|
+
ICON_NODE_EXPANDED = ""
|
|
74
|
+
|
|
75
|
+
|
|
69
76
|
class KaderApp(App):
|
|
70
77
|
"""Main Kader CLI application."""
|
|
71
78
|
|
|
72
79
|
TITLE = "Kader CLI"
|
|
73
|
-
SUB_TITLE = "
|
|
80
|
+
SUB_TITLE = f"v{get_version('kader')}"
|
|
74
81
|
CSS_PATH = "app.tcss"
|
|
75
82
|
|
|
76
83
|
BINDINGS = [
|
|
@@ -171,12 +178,16 @@ class KaderApp(App):
|
|
|
171
178
|
self._confirmation_result = (event.confirmed, None)
|
|
172
179
|
|
|
173
180
|
# Remove selector and show result message
|
|
181
|
+
tool_message = None
|
|
174
182
|
if self._inline_selector:
|
|
183
|
+
tool_message = self._inline_selector.message
|
|
175
184
|
self._inline_selector.remove()
|
|
176
185
|
self._inline_selector = None
|
|
177
186
|
|
|
178
187
|
if event.confirmed:
|
|
179
|
-
|
|
188
|
+
if tool_message:
|
|
189
|
+
conversation.add_message(tool_message, "assistant")
|
|
190
|
+
conversation.add_message("(+) Executing tool...", "assistant")
|
|
180
191
|
# Restart spinner
|
|
181
192
|
try:
|
|
182
193
|
spinner = self.query_one(LoadingSpinner)
|
|
@@ -184,7 +195,7 @@ class KaderApp(App):
|
|
|
184
195
|
except Exception:
|
|
185
196
|
pass
|
|
186
197
|
else:
|
|
187
|
-
conversation.add_message("
|
|
198
|
+
conversation.add_message("(-) Tool execution skipped.", "assistant")
|
|
188
199
|
|
|
189
200
|
# Re-enable input
|
|
190
201
|
prompt_input = self.query_one("#prompt-input", Input)
|
|
@@ -206,7 +217,8 @@ class KaderApp(App):
|
|
|
206
217
|
models = OllamaProvider.get_supported_models()
|
|
207
218
|
if not models:
|
|
208
219
|
conversation.add_message(
|
|
209
|
-
"## Models
|
|
220
|
+
"## Models (^^)\n\n*No models found. Is Ollama running?*",
|
|
221
|
+
"assistant",
|
|
210
222
|
)
|
|
211
223
|
return
|
|
212
224
|
|
|
@@ -224,7 +236,7 @@ class KaderApp(App):
|
|
|
224
236
|
|
|
225
237
|
except Exception as e:
|
|
226
238
|
conversation.add_message(
|
|
227
|
-
f"## Models
|
|
239
|
+
f"## Models (^^)\n\n*Error fetching models: {e}*", "assistant"
|
|
228
240
|
)
|
|
229
241
|
|
|
230
242
|
def on_model_selector_model_selected(
|
|
@@ -244,7 +256,7 @@ class KaderApp(App):
|
|
|
244
256
|
self._agent = self._create_agent(self._current_model)
|
|
245
257
|
|
|
246
258
|
conversation.add_message(
|
|
247
|
-
f"
|
|
259
|
+
f"(+) Model changed from `{old_model}` to `{self._current_model}`",
|
|
248
260
|
"assistant",
|
|
249
261
|
)
|
|
250
262
|
|
|
@@ -281,8 +293,8 @@ class KaderApp(App):
|
|
|
281
293
|
with Horizontal(id="main-container"):
|
|
282
294
|
# Sidebar with directory tree
|
|
283
295
|
with Vertical(id="sidebar"):
|
|
284
|
-
yield Static("
|
|
285
|
-
yield
|
|
296
|
+
yield Static("Files", id="sidebar-title")
|
|
297
|
+
yield ASCIITree(str(Path.cwd().name), id="directory-tree")
|
|
286
298
|
|
|
287
299
|
# Main content area
|
|
288
300
|
with Vertical(id="content-area"):
|
|
@@ -312,9 +324,32 @@ class KaderApp(App):
|
|
|
312
324
|
# Check initial size
|
|
313
325
|
self._check_terminal_size()
|
|
314
326
|
|
|
327
|
+
# Start background update check
|
|
315
328
|
# Start background update check
|
|
316
329
|
threading.Thread(target=self._check_for_updates, daemon=True).start()
|
|
317
330
|
|
|
331
|
+
# Initial tree population
|
|
332
|
+
self._refresh_directory_tree()
|
|
333
|
+
|
|
334
|
+
def _populate_tree(self, node, path: Path) -> None:
|
|
335
|
+
"""Recursively populate the tree with ASCII symbols."""
|
|
336
|
+
try:
|
|
337
|
+
# Sort: directories first, then files
|
|
338
|
+
items = sorted(
|
|
339
|
+
path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())
|
|
340
|
+
)
|
|
341
|
+
for child in items:
|
|
342
|
+
if child.name.startswith((".", "__pycache__")):
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
if child.is_dir():
|
|
346
|
+
new_node = node.add(f"[+] {child.name}", expand=False)
|
|
347
|
+
self._populate_tree(new_node, child)
|
|
348
|
+
else:
|
|
349
|
+
node.add(f"{child.name}")
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
318
353
|
def _check_for_updates(self) -> None:
|
|
319
354
|
"""Check for package updates in background thread."""
|
|
320
355
|
try:
|
|
@@ -365,7 +400,7 @@ class KaderApp(App):
|
|
|
365
400
|
except Exception:
|
|
366
401
|
if too_small:
|
|
367
402
|
# Show warning overlay
|
|
368
|
-
warning_text = f"""
|
|
403
|
+
warning_text = f"""<!> Terminal Too Small
|
|
369
404
|
|
|
370
405
|
Current: {width}x{height}
|
|
371
406
|
Minimum: {MIN_WIDTH}x{MIN_HEIGHT}
|
|
@@ -402,7 +437,7 @@ Please resize your terminal."""
|
|
|
402
437
|
self._cycle_theme()
|
|
403
438
|
theme_name = THEME_NAMES[self._current_theme_index]
|
|
404
439
|
conversation.add_message(
|
|
405
|
-
f"
|
|
440
|
+
f"{{~}} Theme changed to **{theme_name}**!", "assistant"
|
|
406
441
|
)
|
|
407
442
|
elif cmd == "/clear":
|
|
408
443
|
conversation.clear_messages()
|
|
@@ -478,7 +513,7 @@ Please resize your terminal."""
|
|
|
478
513
|
|
|
479
514
|
except Exception as e:
|
|
480
515
|
spinner.stop()
|
|
481
|
-
error_msg = f"
|
|
516
|
+
error_msg = f"(-) **Error:** {str(e)}\n\nMake sure Ollama is running and the model `{self._current_model}` is available."
|
|
482
517
|
conversation.add_message(error_msg, "assistant")
|
|
483
518
|
self.notify(f"Error: {e}", severity="error")
|
|
484
519
|
|
|
@@ -526,10 +561,13 @@ Please resize your terminal."""
|
|
|
526
561
|
self.notify("Directory tree refreshed!", severity="information")
|
|
527
562
|
|
|
528
563
|
def _refresh_directory_tree(self) -> None:
|
|
529
|
-
"""Refresh the directory tree
|
|
564
|
+
"""Refresh the directory tree with ASCII symbols."""
|
|
530
565
|
try:
|
|
531
|
-
tree = self.query_one("#directory-tree",
|
|
532
|
-
tree.
|
|
566
|
+
tree = self.query_one("#directory-tree", ASCIITree)
|
|
567
|
+
tree.clear()
|
|
568
|
+
tree.root.label = str(Path.cwd().name)
|
|
569
|
+
self._populate_tree(tree.root, Path.cwd())
|
|
570
|
+
tree.root.expand()
|
|
533
571
|
except Exception:
|
|
534
572
|
pass # Silently ignore if tree not found
|
|
535
573
|
|
|
@@ -546,12 +584,12 @@ Please resize your terminal."""
|
|
|
546
584
|
self._session_manager.save_conversation(self._current_session_id, messages)
|
|
547
585
|
|
|
548
586
|
conversation.add_message(
|
|
549
|
-
f"
|
|
587
|
+
f"(+) Session saved!\n\n**Session ID:** `{self._current_session_id}`",
|
|
550
588
|
"assistant",
|
|
551
589
|
)
|
|
552
590
|
self.notify("Session saved!", severity="information")
|
|
553
591
|
except Exception as e:
|
|
554
|
-
conversation.add_message(f"
|
|
592
|
+
conversation.add_message(f"(-) Error saving session: {e}", "assistant")
|
|
555
593
|
self.notify(f"Error: {e}", severity="error")
|
|
556
594
|
|
|
557
595
|
def _handle_load_session(
|
|
@@ -563,7 +601,7 @@ Please resize your terminal."""
|
|
|
563
601
|
session = self._session_manager.get_session(session_id)
|
|
564
602
|
if not session:
|
|
565
603
|
conversation.add_message(
|
|
566
|
-
f"
|
|
604
|
+
f"(-) Session `{session_id}` not found.\n\nUse `/sessions` to see available sessions.",
|
|
567
605
|
"assistant",
|
|
568
606
|
)
|
|
569
607
|
return
|
|
@@ -585,12 +623,12 @@ Please resize your terminal."""
|
|
|
585
623
|
|
|
586
624
|
self._current_session_id = session_id
|
|
587
625
|
conversation.add_message(
|
|
588
|
-
f"
|
|
626
|
+
f"(+) Session `{session_id}` loaded with {len(messages)} messages.",
|
|
589
627
|
"assistant",
|
|
590
628
|
)
|
|
591
629
|
self.notify("Session loaded!", severity="information")
|
|
592
630
|
except Exception as e:
|
|
593
|
-
conversation.add_message(f"
|
|
631
|
+
conversation.add_message(f"(-) Error loading session: {e}", "assistant")
|
|
594
632
|
self.notify(f"Error: {e}", severity="error")
|
|
595
633
|
|
|
596
634
|
def _handle_list_sessions(self, conversation: ConversationView) -> None:
|
|
@@ -600,13 +638,13 @@ Please resize your terminal."""
|
|
|
600
638
|
|
|
601
639
|
if not sessions:
|
|
602
640
|
conversation.add_message(
|
|
603
|
-
"
|
|
641
|
+
"[ ] No saved sessions found.\n\nUse `/save` to save the current session.",
|
|
604
642
|
"assistant",
|
|
605
643
|
)
|
|
606
644
|
return
|
|
607
645
|
|
|
608
646
|
lines = [
|
|
609
|
-
"## Saved Sessions
|
|
647
|
+
"## Saved Sessions [=]\n",
|
|
610
648
|
"| Session ID | Created | Updated |",
|
|
611
649
|
"|------------|---------|---------|",
|
|
612
650
|
]
|
|
@@ -631,7 +669,7 @@ Please resize your terminal."""
|
|
|
631
669
|
model = self._agent.provider.model
|
|
632
670
|
|
|
633
671
|
lines = [
|
|
634
|
-
"## Usage Costs
|
|
672
|
+
"## Usage Costs ($)\n",
|
|
635
673
|
f"**Model:** `{model}`\n",
|
|
636
674
|
"### Cost Breakdown",
|
|
637
675
|
"| Type | Amount |",
|
|
@@ -650,12 +688,12 @@ Please resize your terminal."""
|
|
|
650
688
|
|
|
651
689
|
if cost.total_cost == 0.0:
|
|
652
690
|
lines.append(
|
|
653
|
-
"\n>
|
|
691
|
+
"\n> (!) *Note: Ollama runs locally, so there are no API costs.*"
|
|
654
692
|
)
|
|
655
693
|
|
|
656
694
|
conversation.add_message("\n".join(lines), "assistant")
|
|
657
695
|
except Exception as e:
|
|
658
|
-
conversation.add_message(f"
|
|
696
|
+
conversation.add_message(f"(-) Error getting costs: {e}", "assistant")
|
|
659
697
|
self.notify(f"Error: {e}", severity="error")
|
|
660
698
|
|
|
661
699
|
|
|
@@ -42,7 +42,7 @@ Header {
|
|
|
42
42
|
background: $primary;
|
|
43
43
|
color: $text;
|
|
44
44
|
text-style: bold;
|
|
45
|
-
height:
|
|
45
|
+
height: 1;
|
|
46
46
|
dock: top;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -75,9 +75,9 @@ FooterKey > .footer-key--key {
|
|
|
75
75
|
/* ===== Sidebar (Directory Tree) ===== */
|
|
76
76
|
|
|
77
77
|
#sidebar {
|
|
78
|
-
width:
|
|
79
|
-
min-width:
|
|
80
|
-
max-width:
|
|
78
|
+
width: 22;
|
|
79
|
+
min-width: 18;
|
|
80
|
+
max-width: 35;
|
|
81
81
|
background: $surface;
|
|
82
82
|
border-right: thick $primary;
|
|
83
83
|
padding: 0;
|
|
@@ -126,7 +126,6 @@ DirectoryTree:focus > .directory-tree--cursor {
|
|
|
126
126
|
#conversation {
|
|
127
127
|
height: 1fr;
|
|
128
128
|
background: $background;
|
|
129
|
-
border-bottom: thick $surface;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
ConversationView {
|
|
@@ -148,15 +147,15 @@ ConversationView {
|
|
|
148
147
|
|
|
149
148
|
#input-container {
|
|
150
149
|
height: auto;
|
|
151
|
-
min-height:
|
|
152
|
-
max-height:
|
|
153
|
-
background: $
|
|
150
|
+
min-height: 4;
|
|
151
|
+
max-height: 7;
|
|
152
|
+
background: $background;
|
|
154
153
|
padding: 1;
|
|
155
|
-
|
|
154
|
+
margin-bottom: 1;
|
|
156
155
|
}
|
|
157
156
|
|
|
158
157
|
#prompt-input {
|
|
159
|
-
background:
|
|
158
|
+
background: transparent;
|
|
160
159
|
border: round $primary;
|
|
161
160
|
padding: 0 1;
|
|
162
161
|
height: 3;
|
|
@@ -8,7 +8,7 @@ THEME_NAMES = ["dark", "ocean", "forest", "sunset"]
|
|
|
8
8
|
# Default model
|
|
9
9
|
DEFAULT_MODEL = "qwen3-coder:480b-cloud"
|
|
10
10
|
|
|
11
|
-
HELP_TEXT = """## Kader CLI Commands
|
|
11
|
+
HELP_TEXT = """## Kader CLI Commands
|
|
12
12
|
|
|
13
13
|
| Command | Description |
|
|
14
14
|
|---------|-------------|
|
|
@@ -23,7 +23,7 @@ HELP_TEXT = """## Kader CLI Commands 📖
|
|
|
23
23
|
| `/refresh` | Refresh file tree |
|
|
24
24
|
| `/exit` | Exit the CLI |
|
|
25
25
|
|
|
26
|
-
### Keyboard Shortcuts
|
|
26
|
+
### Keyboard Shortcuts
|
|
27
27
|
|
|
28
28
|
| Shortcut | Action |
|
|
29
29
|
|----------|--------|
|
|
@@ -33,7 +33,7 @@ HELP_TEXT = """## Kader CLI Commands 📖
|
|
|
33
33
|
| `Ctrl+R` | Refresh file tree |
|
|
34
34
|
| `Ctrl+Q` | Quit |
|
|
35
35
|
|
|
36
|
-
### Input Editing
|
|
36
|
+
### Input Editing
|
|
37
37
|
|
|
38
38
|
| Shortcut | Action |
|
|
39
39
|
|----------|--------|
|
|
@@ -44,7 +44,8 @@ HELP_TEXT = """## Kader CLI Commands 📖
|
|
|
44
44
|
|
|
45
45
|
### Tips:
|
|
46
46
|
- Type any question to chat with the AI
|
|
47
|
-
- Use **Tab** to navigate between panels
|
|
47
|
+
- Use **Tab** to navigate between panels
|
|
48
|
+
"""
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def get_models_text() -> str:
|
|
@@ -52,12 +53,16 @@ def get_models_text() -> str:
|
|
|
52
53
|
try:
|
|
53
54
|
models = OllamaProvider.get_supported_models()
|
|
54
55
|
if not models:
|
|
55
|
-
return "## Available Models
|
|
56
|
+
return "## Available Models (^^)\n\n*No models found. Is Ollama running?*"
|
|
56
57
|
|
|
57
|
-
lines = [
|
|
58
|
+
lines = [
|
|
59
|
+
"## Available Models (^^)\n",
|
|
60
|
+
"| Model | Status |",
|
|
61
|
+
"|-------|--------|",
|
|
62
|
+
]
|
|
58
63
|
for model in models:
|
|
59
|
-
lines.append(f"| {model} |
|
|
64
|
+
lines.append(f"| {model} | (+) Available |")
|
|
60
65
|
lines.append(f"\n*Currently using: **{DEFAULT_MODEL}***")
|
|
61
66
|
return "\n".join(lines)
|
|
62
67
|
except Exception as e:
|
|
63
|
-
return f"## Available Models
|
|
68
|
+
return f"## Available Models (^^)\n\n*Error fetching models: {e}*"
|
|
@@ -84,12 +84,12 @@ class InlineSelector(Widget, can_focus=True):
|
|
|
84
84
|
def __init__(self, message: str, options: list[str] = None, **kwargs) -> None:
|
|
85
85
|
super().__init__(**kwargs)
|
|
86
86
|
self.message = message
|
|
87
|
-
self.options = options or ["
|
|
87
|
+
self.options = options or ["(+) Yes", "(-) No"]
|
|
88
88
|
|
|
89
89
|
def compose(self) -> ComposeResult:
|
|
90
90
|
from textual.containers import Horizontal
|
|
91
91
|
|
|
92
|
-
yield Static(f"
|
|
92
|
+
yield Static(f">_ {self.message}", classes="message-text")
|
|
93
93
|
yield Static(
|
|
94
94
|
"↑↓ to select • Enter to confirm • Y/N for quick select",
|
|
95
95
|
classes="prompt-text",
|
|
@@ -230,7 +230,7 @@ class ModelSelector(Widget, can_focus=True):
|
|
|
230
230
|
self.selected_index = models.index(current_model)
|
|
231
231
|
|
|
232
232
|
def compose(self) -> ComposeResult:
|
|
233
|
-
yield Static("
|
|
233
|
+
yield Static("(^^) Select Model", classes="title-text")
|
|
234
234
|
yield Static(
|
|
235
235
|
"↑↓ to navigate • Enter to select • Esc to cancel", classes="prompt-text"
|
|
236
236
|
)
|
|
@@ -245,7 +245,7 @@ class ModelSelector(Widget, can_focus=True):
|
|
|
245
245
|
classes += " not-selected"
|
|
246
246
|
if is_current:
|
|
247
247
|
classes += " current"
|
|
248
|
-
label = f"
|
|
248
|
+
label = f" >> {model}" if is_selected else f" {model}"
|
|
249
249
|
if is_current:
|
|
250
250
|
label += " (current)"
|
|
251
251
|
yield Static(label, classes=classes, id=f"model-{i}")
|
|
@@ -272,7 +272,7 @@ class ModelSelector(Widget, can_focus=True):
|
|
|
272
272
|
new_option.remove_class("not-selected")
|
|
273
273
|
new_option.add_class("selected")
|
|
274
274
|
new_model = self.models[new_index]
|
|
275
|
-
new_label = f"
|
|
275
|
+
new_label = f" >> {new_model}"
|
|
276
276
|
if new_model == self.current_model:
|
|
277
277
|
new_label += " (current)"
|
|
278
278
|
new_option.update(new_label)
|
|
@@ -15,7 +15,7 @@ class Message(Static):
|
|
|
15
15
|
self.add_class(f"message-{role}")
|
|
16
16
|
|
|
17
17
|
def compose(self) -> ComposeResult:
|
|
18
|
-
prefix = "
|
|
18
|
+
prefix = "(**) **You:**" if self.role == "user" else "(^^) **Kader:**"
|
|
19
19
|
yield Markdown(f"{prefix}\n\n{self.content}")
|
|
20
20
|
|
|
21
21
|
|
|
@@ -21,7 +21,7 @@ class LoadingSpinner(Static):
|
|
|
21
21
|
}
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
SPINNER_FRAMES = ["
|
|
24
|
+
SPINNER_FRAMES = ["= ", "== ", "=== ", " ===", " ==", " =", " "]
|
|
25
25
|
|
|
26
26
|
frame_index: reactive[int] = reactive(0)
|
|
27
27
|
is_spinning: reactive[bool] = reactive(False)
|
|
@@ -322,7 +322,7 @@ class BaseAgent:
|
|
|
322
322
|
return f"execute {tool_name}"
|
|
323
323
|
|
|
324
324
|
def _confirm_tool_execution(
|
|
325
|
-
self, tool_call_dict: dict
|
|
325
|
+
self, tool_call_dict: dict, llm_content: Optional[str] = None
|
|
326
326
|
) -> tuple[bool, Optional[str]]:
|
|
327
327
|
"""
|
|
328
328
|
Ask user for confirmation before executing a tool.
|
|
@@ -336,6 +336,9 @@ class BaseAgent:
|
|
|
336
336
|
"""
|
|
337
337
|
display_str = self._format_tool_call_for_display(tool_call_dict)
|
|
338
338
|
|
|
339
|
+
if llm_content and len(llm_content) > 0:
|
|
340
|
+
display_str = f"{llm_content}\n\n{display_str}"
|
|
341
|
+
|
|
339
342
|
# Use callback if provided (e.g., for GUI/TUI)
|
|
340
343
|
if self.tool_confirmation_callback:
|
|
341
344
|
return self.tool_confirmation_callback(display_str)
|
|
@@ -357,7 +360,7 @@ class BaseAgent:
|
|
|
357
360
|
print("Please enter 'yes' or 'no'.")
|
|
358
361
|
|
|
359
362
|
async def _aconfirm_tool_execution(
|
|
360
|
-
self, tool_call_dict: dict
|
|
363
|
+
self, tool_call_dict: dict, llm_content: Optional[str] = None
|
|
361
364
|
) -> tuple[bool, Optional[str]]:
|
|
362
365
|
"""
|
|
363
366
|
Async version - Ask user for confirmation before executing a tool.
|
|
@@ -375,7 +378,9 @@ class BaseAgent:
|
|
|
375
378
|
# In production, use asyncio.to_thread or aioconsole
|
|
376
379
|
import asyncio
|
|
377
380
|
|
|
378
|
-
return await asyncio.to_thread(
|
|
381
|
+
return await asyncio.to_thread(
|
|
382
|
+
self._confirm_tool_execution, tool_call_dict, llm_content
|
|
383
|
+
)
|
|
379
384
|
|
|
380
385
|
def _process_tool_calls(
|
|
381
386
|
self, response: LLMResponse
|
|
@@ -396,7 +401,7 @@ class BaseAgent:
|
|
|
396
401
|
# Check for interrupt before tool execution
|
|
397
402
|
if self.interrupt_before_tool:
|
|
398
403
|
should_execute, user_input = self._confirm_tool_execution(
|
|
399
|
-
tool_call_dict
|
|
404
|
+
tool_call_dict, response.content
|
|
400
405
|
)
|
|
401
406
|
if not should_execute:
|
|
402
407
|
# Return the user's elaboration to be processed
|
|
@@ -455,7 +460,7 @@ class BaseAgent:
|
|
|
455
460
|
# Check for interrupt before tool execution
|
|
456
461
|
if self.interrupt_before_tool:
|
|
457
462
|
should_execute, user_input = await self._aconfirm_tool_execution(
|
|
458
|
-
tool_call_dict
|
|
463
|
+
tool_call_dict, response.content
|
|
459
464
|
)
|
|
460
465
|
if not should_execute:
|
|
461
466
|
return (False, user_input)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|