cade-cli 0.3.3__py3-none-any.whl
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.
- cade_cli-0.3.3.dist-info/METADATA +151 -0
- cade_cli-0.3.3.dist-info/RECORD +44 -0
- cade_cli-0.3.3.dist-info/WHEEL +4 -0
- cade_cli-0.3.3.dist-info/entry_points.txt +2 -0
- cadecoder/__init__.py +1 -0
- cadecoder/ai/__init__.py +6 -0
- cadecoder/ai/prompts.py +572 -0
- cadecoder/cli/__init__.py +0 -0
- cadecoder/cli/app.py +147 -0
- cadecoder/cli/auth.py +483 -0
- cadecoder/cli/commands/__init__.py +5 -0
- cadecoder/cli/commands/auth.py +143 -0
- cadecoder/cli/commands/chat.py +264 -0
- cadecoder/cli/commands/mcp.py +477 -0
- cadecoder/cli/commands/tools.py +226 -0
- cadecoder/core/__init__.py +12 -0
- cadecoder/core/config.py +380 -0
- cadecoder/core/constants.py +281 -0
- cadecoder/core/errors.py +145 -0
- cadecoder/core/logging.py +148 -0
- cadecoder/core/types.py +235 -0
- cadecoder/core/utils.py +279 -0
- cadecoder/execution/__init__.py +46 -0
- cadecoder/execution/context_window.py +521 -0
- cadecoder/execution/orchestrator.py +562 -0
- cadecoder/execution/parallel.py +287 -0
- cadecoder/providers/__init__.py +60 -0
- cadecoder/providers/base.py +294 -0
- cadecoder/providers/openai.py +251 -0
- cadecoder/storage/__init__.py +0 -0
- cadecoder/storage/threads.py +489 -0
- cadecoder/templates/login_failed.html +21 -0
- cadecoder/templates/login_success.html +21 -0
- cadecoder/templates/styles.css +87 -0
- cadecoder/tools/__init__.py +19 -0
- cadecoder/tools/builtin.py +644 -0
- cadecoder/tools/filesystem.py +315 -0
- cadecoder/tools/git.py +221 -0
- cadecoder/tools/manager.py +1635 -0
- cadecoder/ui/__init__.py +7 -0
- cadecoder/ui/display.py +338 -0
- cadecoder/ui/input.py +145 -0
- cadecoder/ui/session.py +455 -0
- cadecoder/ui/state.py +20 -0
cadecoder/ui/__init__.py
ADDED
cadecoder/ui/display.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Display helper functions for the TUI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich import box
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from cadecoder.core.constants import UI_STYLE
|
|
14
|
+
from cadecoder.core.logging import get_log_file_path
|
|
15
|
+
|
|
16
|
+
# Console instance - let Rich auto-detect terminal width
|
|
17
|
+
console = Console(stderr=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _format_result_summary(result: str, max_len: int = 80) -> str:
|
|
21
|
+
"""Format a tool result into a brief summary.
|
|
22
|
+
|
|
23
|
+
Intelligently summarizes JSON structures without hardcoded field names.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
result: Raw result string (often JSON)
|
|
27
|
+
max_len: Maximum summary length
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Brief summary string
|
|
31
|
+
"""
|
|
32
|
+
if not result:
|
|
33
|
+
return "[dim]empty[/dim]"
|
|
34
|
+
|
|
35
|
+
# Check for error patterns
|
|
36
|
+
result_lower = result.lower()
|
|
37
|
+
if "failed" in result_lower[:100] or "error" in result_lower[:100]:
|
|
38
|
+
# Extract HTTP status if present
|
|
39
|
+
for code in ["404", "401", "403", "500", "502", "503"]:
|
|
40
|
+
if code in result:
|
|
41
|
+
return f"[red]{code} error[/red]"
|
|
42
|
+
return "[red]failed[/red]"
|
|
43
|
+
|
|
44
|
+
# Try to parse as JSON
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(result)
|
|
47
|
+
return _summarize_json(data)
|
|
48
|
+
except (json.JSONDecodeError, TypeError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# Fallback: truncate raw string
|
|
52
|
+
preview = result.replace("\n", " ").strip()
|
|
53
|
+
if len(preview) > max_len:
|
|
54
|
+
return preview[: max_len - 3] + "..."
|
|
55
|
+
return preview if preview else "[dim]empty[/dim]"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _summarize_json(data: Any, depth: int = 0) -> str:
|
|
59
|
+
"""Recursively summarize a JSON structure.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
data: Parsed JSON data
|
|
63
|
+
depth: Current recursion depth
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Brief summary string
|
|
67
|
+
"""
|
|
68
|
+
if depth > 2:
|
|
69
|
+
return "..."
|
|
70
|
+
|
|
71
|
+
if data is None:
|
|
72
|
+
return "null"
|
|
73
|
+
|
|
74
|
+
if isinstance(data, bool):
|
|
75
|
+
return "✓" if data else "✗"
|
|
76
|
+
|
|
77
|
+
if isinstance(data, int | float):
|
|
78
|
+
return str(data)
|
|
79
|
+
|
|
80
|
+
if isinstance(data, str):
|
|
81
|
+
if len(data) > 40:
|
|
82
|
+
return f'"{data[:37]}..."'
|
|
83
|
+
return f'"{data}"' if len(data) < 20 else data[:40]
|
|
84
|
+
|
|
85
|
+
if isinstance(data, list):
|
|
86
|
+
length = len(data)
|
|
87
|
+
if length == 0:
|
|
88
|
+
return "[]"
|
|
89
|
+
# Peek at first item type
|
|
90
|
+
first = data[0]
|
|
91
|
+
if isinstance(first, dict):
|
|
92
|
+
return f"{length} items"
|
|
93
|
+
if isinstance(first, str):
|
|
94
|
+
return f"{length} strings"
|
|
95
|
+
return f"{length} items"
|
|
96
|
+
|
|
97
|
+
if isinstance(data, dict):
|
|
98
|
+
if not data:
|
|
99
|
+
return "{}"
|
|
100
|
+
|
|
101
|
+
# Count totals
|
|
102
|
+
total_lists = 0
|
|
103
|
+
total_items = 0
|
|
104
|
+
for v in data.values():
|
|
105
|
+
if isinstance(v, list):
|
|
106
|
+
total_lists += 1
|
|
107
|
+
total_items += len(v)
|
|
108
|
+
|
|
109
|
+
# If there are lists, summarize by total items
|
|
110
|
+
if total_lists > 0:
|
|
111
|
+
return f"{total_items} items" if total_items > 0 else "OK"
|
|
112
|
+
|
|
113
|
+
# Otherwise show key count
|
|
114
|
+
key_count = len(data)
|
|
115
|
+
if key_count <= 3:
|
|
116
|
+
return ", ".join(data.keys())
|
|
117
|
+
return f"{key_count} fields"
|
|
118
|
+
|
|
119
|
+
return str(type(data).__name__)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Control signals that should be hidden from display but kept for continuation logic
|
|
123
|
+
CONTROL_SIGNAL_PATTERN = re.compile(
|
|
124
|
+
r"\[(?:TASK_COMPLETE|CONTINUE|NEED_USER_INPUT)\]", re.IGNORECASE
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def strip_control_signals(content: str, strip_whitespace: bool = False) -> str:
|
|
129
|
+
"""Remove control signals from content for display.
|
|
130
|
+
|
|
131
|
+
Strips [TASK_COMPLETE], [CONTINUE], [NEED_USER_INPUT] markers
|
|
132
|
+
that are used by the continuation strategy but shouldn't be
|
|
133
|
+
shown to the user.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
content: Raw content that may contain control signals
|
|
137
|
+
strip_whitespace: If True, also strip leading/trailing whitespace.
|
|
138
|
+
Set to False when processing streaming chunks to
|
|
139
|
+
preserve word spacing.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Content with control signals removed
|
|
143
|
+
"""
|
|
144
|
+
if not content:
|
|
145
|
+
return content
|
|
146
|
+
# Remove the control signals
|
|
147
|
+
cleaned = CONTROL_SIGNAL_PATTERN.sub("", content)
|
|
148
|
+
# Clean up any resulting triple+ newlines
|
|
149
|
+
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
|
150
|
+
if strip_whitespace:
|
|
151
|
+
cleaned = cleaned.strip()
|
|
152
|
+
return cleaned
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def clear_screen() -> None:
|
|
156
|
+
"""Clear the terminal screen."""
|
|
157
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def display_thread_header(thread_name: str) -> None:
|
|
161
|
+
"""Display the thread header/title."""
|
|
162
|
+
if UI_STYLE == "minimal":
|
|
163
|
+
console.print(f"[bold green]Chat Thread:[/bold green] {thread_name}")
|
|
164
|
+
console.print("[dim]/help for commands, Ctrl+C to exit[/dim]")
|
|
165
|
+
console.print()
|
|
166
|
+
else:
|
|
167
|
+
console.print(
|
|
168
|
+
Panel(
|
|
169
|
+
f"[bold green]{thread_name}[/bold green]",
|
|
170
|
+
title="Chat Thread",
|
|
171
|
+
border_style="green",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def display_git_branch_info(branch_name: str | None) -> None:
|
|
177
|
+
"""Display the current git branch information."""
|
|
178
|
+
if branch_name:
|
|
179
|
+
console.print(f"[dim]Git branch: {branch_name}[/dim]")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def display_help() -> None:
|
|
183
|
+
"""Display help information."""
|
|
184
|
+
help_text = """
|
|
185
|
+
[bold cyan]Available Commands:[/bold cyan]
|
|
186
|
+
|
|
187
|
+
[bold]/help[/bold] - Show this help message
|
|
188
|
+
[bold]/exit[/bold], [bold]/quit[/bold] - Exit the chat
|
|
189
|
+
[bold]/clear[/bold] - Clear the screen
|
|
190
|
+
[bold]/tools[/bold] - Show available tools
|
|
191
|
+
[bold]/logs[/bold] - Show log file location and recent logs
|
|
192
|
+
[bold]/context[/bold] - Show context window status and token usage
|
|
193
|
+
[bold]/history[/bold] - Show conversation history
|
|
194
|
+
[bold]/model[/bold] - Show current AI model
|
|
195
|
+
[bold]/thread[/bold] - Show current thread info
|
|
196
|
+
[bold]/pwd[/bold] - Show current directory
|
|
197
|
+
[bold]/cd <path>[/bold] - Change directory
|
|
198
|
+
[bold]/! <cmd>[/bold] - Execute shell command
|
|
199
|
+
|
|
200
|
+
[bold cyan]Context References:[/bold cyan]
|
|
201
|
+
|
|
202
|
+
Use @<path> to include file or directory content as context.
|
|
203
|
+
Examples:
|
|
204
|
+
@src/main.py - Include a specific file
|
|
205
|
+
@src/ - Include directory listing
|
|
206
|
+
@. - Include current directory listing
|
|
207
|
+
|
|
208
|
+
[bold cyan]Keyboard Shortcuts:[/bold cyan]
|
|
209
|
+
|
|
210
|
+
Ctrl+C - Cancel current operation
|
|
211
|
+
Ctrl+C Ctrl+C - Exit chat (press twice quickly)
|
|
212
|
+
Ctrl+D - Exit chat
|
|
213
|
+
Up/Down arrows - Navigate message history
|
|
214
|
+
"""
|
|
215
|
+
console.print(Panel(help_text, title="Help", border_style="cyan"))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def display_logs(num_lines: int = 20) -> None:
|
|
219
|
+
"""Display the log file location and recent log entries."""
|
|
220
|
+
log_path = get_log_file_path()
|
|
221
|
+
|
|
222
|
+
console.print(f"[bold cyan]Log file:[/bold cyan] {log_path}")
|
|
223
|
+
console.print()
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
if log_path.exists():
|
|
227
|
+
with open(log_path, encoding="utf-8") as f:
|
|
228
|
+
lines = f.readlines()
|
|
229
|
+
recent_lines = lines[-num_lines:] if len(lines) > num_lines else lines
|
|
230
|
+
|
|
231
|
+
if recent_lines:
|
|
232
|
+
console.print(f"[dim]Last {len(recent_lines)} log entries:[/dim]")
|
|
233
|
+
console.print()
|
|
234
|
+
for line in recent_lines:
|
|
235
|
+
line = line.strip()
|
|
236
|
+
if not line:
|
|
237
|
+
continue
|
|
238
|
+
# Color code by log level
|
|
239
|
+
if "ERROR" in line:
|
|
240
|
+
console.print(f"[red]{line}[/red]")
|
|
241
|
+
elif "WARNING" in line:
|
|
242
|
+
console.print(f"[yellow]{line}[/yellow]")
|
|
243
|
+
elif "INFO" in line:
|
|
244
|
+
console.print(f"[green]{line}[/green]")
|
|
245
|
+
else:
|
|
246
|
+
console.print(f"[dim]{line}[/dim]")
|
|
247
|
+
else:
|
|
248
|
+
console.print("[dim]Log file is empty.[/dim]")
|
|
249
|
+
else:
|
|
250
|
+
console.print("[yellow]Log file does not exist yet.[/yellow]")
|
|
251
|
+
except Exception as e:
|
|
252
|
+
console.print(f"[red]Error reading log file: {e}[/red]")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def display_messages(messages: list[Any], limit: int = 10) -> None:
|
|
256
|
+
"""Display recent messages."""
|
|
257
|
+
if not messages:
|
|
258
|
+
console.print("[dim]No messages in this thread.[/dim]")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
recent = messages[-limit:]
|
|
262
|
+
for msg in recent:
|
|
263
|
+
role = getattr(msg, "role", "unknown")
|
|
264
|
+
content = getattr(msg, "content", "")
|
|
265
|
+
|
|
266
|
+
if role == "user":
|
|
267
|
+
console.print(f"[bold blue]You:[/bold blue] {content}")
|
|
268
|
+
elif role == "assistant":
|
|
269
|
+
console.print(f"[bold green]Assistant:[/bold green] {content[:500]}...")
|
|
270
|
+
elif role == "system":
|
|
271
|
+
console.print(f"[dim]System: {content[:100]}...[/dim]")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def display_tools_async(tool_manager: Any) -> None:
|
|
275
|
+
"""Display available tools."""
|
|
276
|
+
if not tool_manager:
|
|
277
|
+
console.print("[yellow]No tool manager available.[/yellow]")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
tools = await tool_manager.get_tools()
|
|
282
|
+
if not tools:
|
|
283
|
+
console.print("[yellow]No tools available.[/yellow]")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
table = Table(title="Available Tools", box=box.ROUNDED)
|
|
287
|
+
table.add_column("Name", style="cyan")
|
|
288
|
+
table.add_column("Description", style="white")
|
|
289
|
+
table.add_column("Source", style="dim")
|
|
290
|
+
|
|
291
|
+
for tool in tools[:30]: # Limit display
|
|
292
|
+
func_info = tool.get("function", {})
|
|
293
|
+
name = func_info.get("name", "Unknown")
|
|
294
|
+
desc = func_info.get("description", "No description")
|
|
295
|
+
|
|
296
|
+
# Truncate description
|
|
297
|
+
if len(desc) > 60:
|
|
298
|
+
desc = desc[:57] + "..."
|
|
299
|
+
|
|
300
|
+
# Determine source
|
|
301
|
+
source = "Remote" if "[Arcade Cloud]" in desc else "Local"
|
|
302
|
+
desc = desc.replace("[Arcade Cloud] ", "")
|
|
303
|
+
|
|
304
|
+
table.add_row(name, desc, source)
|
|
305
|
+
|
|
306
|
+
console.print(table)
|
|
307
|
+
console.print(f"[dim]Total: {len(tools)} tools[/dim]")
|
|
308
|
+
except Exception as e:
|
|
309
|
+
console.print(f"[red]Error loading tools: {e}[/red]")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def display_tool_result(name: str, content: Any, status: str = "success") -> None:
|
|
313
|
+
"""Display a tool execution result as a single compact line."""
|
|
314
|
+
content_str = str(content)
|
|
315
|
+
summary = _format_result_summary(content_str)
|
|
316
|
+
|
|
317
|
+
# Check if it's an error
|
|
318
|
+
is_error = "Failed to execute" in content_str or status != "success" or "[red]" in summary
|
|
319
|
+
|
|
320
|
+
if is_error:
|
|
321
|
+
icon = "[red]✗[/red]"
|
|
322
|
+
name_style = f"[red]{name}[/red]"
|
|
323
|
+
else:
|
|
324
|
+
icon = "[green]✓[/green]"
|
|
325
|
+
name_style = f"[cyan]{name}[/cyan]"
|
|
326
|
+
|
|
327
|
+
console.print(f"{icon} {name_style}: {summary}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def display_tool_error(name: str, error: str) -> None:
|
|
331
|
+
"""Display a tool execution error as a compact line."""
|
|
332
|
+
# Extract brief error message
|
|
333
|
+
brief_error = error
|
|
334
|
+
if len(brief_error) > 80:
|
|
335
|
+
brief_error = brief_error[:77] + "..."
|
|
336
|
+
brief_error = brief_error.replace("\n", " ")
|
|
337
|
+
|
|
338
|
+
console.print(f"[red]✗ {name}[/red]: [dim]{brief_error}[/dim]")
|
cadecoder/ui/input.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Input handling for the TUI."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit import PromptSession
|
|
7
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
8
|
+
from prompt_toolkit.history import History
|
|
9
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
10
|
+
from prompt_toolkit.keys import Keys
|
|
11
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
12
|
+
from prompt_toolkit.styles import Style
|
|
13
|
+
|
|
14
|
+
from cadecoder.core.logging import log
|
|
15
|
+
from cadecoder.ui.state import TuiState
|
|
16
|
+
|
|
17
|
+
# --- Prompt History ---
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InMemoryPromptHistory(History):
|
|
21
|
+
"""Simple in-memory history for prompt session."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, thread_id: str | None = None):
|
|
24
|
+
"""Initialize history.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
thread_id: Optional thread ID (unused, kept for API compatibility)
|
|
28
|
+
"""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._history: list[str] = []
|
|
31
|
+
|
|
32
|
+
def load_history_strings(self):
|
|
33
|
+
"""Load history strings."""
|
|
34
|
+
return iter(self._history)
|
|
35
|
+
|
|
36
|
+
def store_string(self, string: str) -> None:
|
|
37
|
+
"""Store a string in history."""
|
|
38
|
+
if string.strip() and string not in self._history:
|
|
39
|
+
self._history.append(string)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --- Key Bindings ---
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_key_bindings(
|
|
46
|
+
cancel_callback: Callable[[], None] | None = None,
|
|
47
|
+
exit_callback: Callable[[], None] | None = None,
|
|
48
|
+
) -> KeyBindings:
|
|
49
|
+
"""Create key bindings for the prompt."""
|
|
50
|
+
kb = KeyBindings()
|
|
51
|
+
|
|
52
|
+
@kb.add(Keys.ControlC)
|
|
53
|
+
def handle_ctrl_c(event):
|
|
54
|
+
"""Handle Ctrl+C."""
|
|
55
|
+
if cancel_callback:
|
|
56
|
+
cancel_callback()
|
|
57
|
+
else:
|
|
58
|
+
event.app.exit(result=None)
|
|
59
|
+
|
|
60
|
+
@kb.add(Keys.ControlD)
|
|
61
|
+
def handle_ctrl_d(event):
|
|
62
|
+
"""Handle Ctrl+D."""
|
|
63
|
+
if exit_callback:
|
|
64
|
+
exit_callback()
|
|
65
|
+
else:
|
|
66
|
+
event.app.exit(result=None)
|
|
67
|
+
|
|
68
|
+
return kb
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --- Bottom Toolbar ---
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_bottom_toolbar(state: TuiState) -> Callable[[], FormattedText]:
|
|
75
|
+
"""Create a bottom toolbar function."""
|
|
76
|
+
|
|
77
|
+
def get_toolbar() -> FormattedText:
|
|
78
|
+
mode = state.chat_mode.upper()
|
|
79
|
+
return FormattedText(
|
|
80
|
+
[
|
|
81
|
+
("class:toolbar", f" {mode} "),
|
|
82
|
+
("class:toolbar-separator", " │ "),
|
|
83
|
+
("class:toolbar", "Ctrl+C: Cancel, Ctrl+D: Exit"),
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return get_toolbar
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --- Prompt Session ---
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
PROMPT_STYLE = Style.from_dict(
|
|
94
|
+
{
|
|
95
|
+
"prompt": "bold cyan",
|
|
96
|
+
"toolbar": "bg:#333333 #ffffff",
|
|
97
|
+
"toolbar-separator": "bg:#333333 #666666",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_prompt_session(
|
|
103
|
+
history: History | None = None,
|
|
104
|
+
key_bindings: KeyBindings | None = None,
|
|
105
|
+
bottom_toolbar: Callable[[], FormattedText] | None = None,
|
|
106
|
+
completer: Any = None,
|
|
107
|
+
) -> PromptSession:
|
|
108
|
+
"""Create a prompt session."""
|
|
109
|
+
return PromptSession(
|
|
110
|
+
history=history,
|
|
111
|
+
key_bindings=key_bindings,
|
|
112
|
+
bottom_toolbar=bottom_toolbar,
|
|
113
|
+
style=PROMPT_STYLE,
|
|
114
|
+
completer=completer,
|
|
115
|
+
complete_while_typing=False,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- Async Prompt Wrapper ---
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AsyncPromptWrapper:
|
|
123
|
+
"""Wrapper for async prompt operations."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, session: PromptSession):
|
|
126
|
+
self.session = session
|
|
127
|
+
|
|
128
|
+
async def prompt_async(
|
|
129
|
+
self,
|
|
130
|
+
message: str = "> ",
|
|
131
|
+
default: str = "",
|
|
132
|
+
) -> str | None:
|
|
133
|
+
"""Prompt for input asynchronously."""
|
|
134
|
+
try:
|
|
135
|
+
with patch_stdout():
|
|
136
|
+
result = await self.session.prompt_async(
|
|
137
|
+
message=message,
|
|
138
|
+
default=default,
|
|
139
|
+
)
|
|
140
|
+
return result
|
|
141
|
+
except (EOFError, KeyboardInterrupt):
|
|
142
|
+
return None
|
|
143
|
+
except Exception as e:
|
|
144
|
+
log.error(f"Prompt error: {e}")
|
|
145
|
+
return None
|