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/session.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""Main chat session for the TUI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from ulid import ulid
|
|
13
|
+
|
|
14
|
+
from cadecoder.core.constants import DEFAULT_AI_MODEL
|
|
15
|
+
from cadecoder.core.errors import AuthError
|
|
16
|
+
from cadecoder.core.logging import log
|
|
17
|
+
from cadecoder.core.types import ExecutionEventType
|
|
18
|
+
from cadecoder.execution.orchestrator import (
|
|
19
|
+
ExecutionContext,
|
|
20
|
+
create_orchestrator,
|
|
21
|
+
)
|
|
22
|
+
from cadecoder.storage.threads import (
|
|
23
|
+
Message,
|
|
24
|
+
ModelInfo,
|
|
25
|
+
ToolCallInfo,
|
|
26
|
+
get_thread_history,
|
|
27
|
+
)
|
|
28
|
+
from cadecoder.tools.git import get_current_branch_name
|
|
29
|
+
from cadecoder.ui.display import (
|
|
30
|
+
clear_screen,
|
|
31
|
+
console,
|
|
32
|
+
display_git_branch_info,
|
|
33
|
+
display_help,
|
|
34
|
+
display_logs,
|
|
35
|
+
display_messages,
|
|
36
|
+
display_thread_header,
|
|
37
|
+
display_tool_result,
|
|
38
|
+
display_tools_async,
|
|
39
|
+
strip_control_signals,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Box-drawing characters that break Rich Markdown rendering
|
|
43
|
+
_BOX_CHARS = frozenset("┌┐└┘├┤┬┴┼─│═║╔╗╚╝╠╣╦╩╬")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _has_box_chars(text: str) -> bool:
|
|
47
|
+
"""Check if text contains box-drawing characters."""
|
|
48
|
+
return bool(_BOX_CHARS & set(text))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ChatSession:
|
|
52
|
+
"""Manages a chat session with the AI agent."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
thread_id: str,
|
|
57
|
+
model: str = DEFAULT_AI_MODEL,
|
|
58
|
+
system_prompt: str | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Initialize chat session."""
|
|
61
|
+
self.thread_id = thread_id
|
|
62
|
+
self.model = model
|
|
63
|
+
self.system_prompt = system_prompt
|
|
64
|
+
self.history_manager = get_thread_history()
|
|
65
|
+
self.thread = self.history_manager.get_thread(thread_id)
|
|
66
|
+
self.orchestrator = create_orchestrator(default_model=model)
|
|
67
|
+
self.current_task: asyncio.Task | None = None
|
|
68
|
+
self._ctrl_c_count = 0
|
|
69
|
+
|
|
70
|
+
def get_conversation_history(self) -> list[dict[str, Any]]:
|
|
71
|
+
"""Get conversation history in LLM format."""
|
|
72
|
+
messages = self.history_manager.get_messages(self.thread_id)
|
|
73
|
+
history: list[dict[str, Any]] = []
|
|
74
|
+
|
|
75
|
+
for msg in messages:
|
|
76
|
+
if msg.role == "system":
|
|
77
|
+
history.append({"role": "system", "content": msg.content or ""})
|
|
78
|
+
elif msg.role == "user":
|
|
79
|
+
history.append({"role": "user", "content": msg.content or ""})
|
|
80
|
+
elif msg.role == "assistant":
|
|
81
|
+
entry: dict[str, Any] = {"role": "assistant", "content": msg.content}
|
|
82
|
+
if msg.tool_calls:
|
|
83
|
+
entry["tool_calls"] = [
|
|
84
|
+
{
|
|
85
|
+
"id": tc.call_id,
|
|
86
|
+
"type": "function",
|
|
87
|
+
"function": {
|
|
88
|
+
"name": tc.tool_name,
|
|
89
|
+
"arguments": json.dumps(tc.parameters),
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
for tc in msg.tool_calls
|
|
93
|
+
]
|
|
94
|
+
history.append(entry)
|
|
95
|
+
elif msg.role == "tool":
|
|
96
|
+
history.append(
|
|
97
|
+
{
|
|
98
|
+
"role": "tool",
|
|
99
|
+
"tool_call_id": msg.responding_tool_call_id or "",
|
|
100
|
+
"content": msg.content or "",
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return history
|
|
105
|
+
|
|
106
|
+
def save_user_message(self, content: str) -> Message:
|
|
107
|
+
"""Save a user message."""
|
|
108
|
+
message = Message(
|
|
109
|
+
id=str(ulid()).lower(),
|
|
110
|
+
thread_id=self.thread_id,
|
|
111
|
+
role="user",
|
|
112
|
+
content=content,
|
|
113
|
+
)
|
|
114
|
+
self.history_manager.add_message(message)
|
|
115
|
+
return message
|
|
116
|
+
|
|
117
|
+
def save_assistant_message(
|
|
118
|
+
self,
|
|
119
|
+
content: str | None,
|
|
120
|
+
tool_calls: list[dict[str, Any]] | None = None,
|
|
121
|
+
model_info: ModelInfo | None = None,
|
|
122
|
+
) -> Message:
|
|
123
|
+
"""Save an assistant message."""
|
|
124
|
+
tc_infos = []
|
|
125
|
+
if tool_calls:
|
|
126
|
+
for tc in tool_calls:
|
|
127
|
+
func = tc.get("function", {})
|
|
128
|
+
args_raw = func.get("arguments", "{}")
|
|
129
|
+
if isinstance(args_raw, str):
|
|
130
|
+
try:
|
|
131
|
+
params = json.loads(args_raw)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
params = {"raw": args_raw}
|
|
134
|
+
else:
|
|
135
|
+
params = args_raw if isinstance(args_raw, dict) else {}
|
|
136
|
+
|
|
137
|
+
tc_infos.append(
|
|
138
|
+
ToolCallInfo(
|
|
139
|
+
call_id=tc.get("id", str(ulid()).lower()),
|
|
140
|
+
tool_name=func.get("name", "unknown"),
|
|
141
|
+
parameters=params,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
message = Message(
|
|
146
|
+
id=str(ulid()).lower(),
|
|
147
|
+
thread_id=self.thread_id,
|
|
148
|
+
role="assistant",
|
|
149
|
+
content=content,
|
|
150
|
+
tool_calls=tc_infos,
|
|
151
|
+
model_info=model_info,
|
|
152
|
+
)
|
|
153
|
+
self.history_manager.add_message(message)
|
|
154
|
+
return message
|
|
155
|
+
|
|
156
|
+
def save_tool_message(self, tool_call_id: str, content: str, tool_name: str) -> Message:
|
|
157
|
+
"""Save a tool response message."""
|
|
158
|
+
message = Message(
|
|
159
|
+
id=str(ulid()).lower(),
|
|
160
|
+
thread_id=self.thread_id,
|
|
161
|
+
role="tool",
|
|
162
|
+
content=content,
|
|
163
|
+
responding_tool_call_id=tool_call_id,
|
|
164
|
+
)
|
|
165
|
+
self.history_manager.add_message(message)
|
|
166
|
+
return message
|
|
167
|
+
|
|
168
|
+
async def process_input(self, user_input: str) -> None:
|
|
169
|
+
"""Process user input."""
|
|
170
|
+
if user_input.startswith("/"):
|
|
171
|
+
await self.handle_command(user_input)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
history = self.get_conversation_history()
|
|
175
|
+
self.save_user_message(user_input)
|
|
176
|
+
|
|
177
|
+
context = ExecutionContext(task=user_input, conversation_history=history)
|
|
178
|
+
accumulated_content = ""
|
|
179
|
+
tool_calls: list[dict[str, Any]] = []
|
|
180
|
+
tool_results: list[tuple[str, str, str]] = []
|
|
181
|
+
|
|
182
|
+
console.print()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
with Live(console=console, refresh_per_second=10) as live:
|
|
186
|
+
async for event in self.orchestrator.stream(context):
|
|
187
|
+
if event.type == ExecutionEventType.CONTENT:
|
|
188
|
+
accumulated_content += event.content or ""
|
|
189
|
+
display_content = strip_control_signals(
|
|
190
|
+
accumulated_content, strip_whitespace=True
|
|
191
|
+
)
|
|
192
|
+
if display_content:
|
|
193
|
+
# Plain text for box chars (Markdown breaks them)
|
|
194
|
+
if _has_box_chars(display_content):
|
|
195
|
+
live.update(display_content)
|
|
196
|
+
else:
|
|
197
|
+
live.update(Markdown(display_content))
|
|
198
|
+
|
|
199
|
+
elif event.type == ExecutionEventType.TOOL_CALL:
|
|
200
|
+
tc = event.metadata.get("tool_call", {})
|
|
201
|
+
tool_calls.append(tc)
|
|
202
|
+
func = tc.get("function", {})
|
|
203
|
+
tool_name = func.get("name", "unknown")
|
|
204
|
+
console.print(f"[dim]Calling tool: {tool_name}[/dim]")
|
|
205
|
+
|
|
206
|
+
elif event.type == ExecutionEventType.TOOL_RESULT:
|
|
207
|
+
tool_name = event.metadata.get("tool_name", "unknown")
|
|
208
|
+
tool_call_id = event.metadata.get("tool_call_id", "")
|
|
209
|
+
result_content = event.content or ""
|
|
210
|
+
display_tool_result(tool_name, result_content)
|
|
211
|
+
tool_results.append((tool_call_id, tool_name, result_content))
|
|
212
|
+
|
|
213
|
+
except asyncio.CancelledError:
|
|
214
|
+
console.print("\n[yellow]Operation cancelled.[/yellow]")
|
|
215
|
+
return
|
|
216
|
+
except Exception as e:
|
|
217
|
+
console.print(f"\n[red]Error: {e}[/red]")
|
|
218
|
+
log.error(f"Error processing input: {e}", exc_info=True)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Save assistant message
|
|
222
|
+
if accumulated_content or tool_calls:
|
|
223
|
+
self.save_assistant_message(accumulated_content, tool_calls)
|
|
224
|
+
|
|
225
|
+
# Save tool results
|
|
226
|
+
for tool_call_id, tool_name, result_content in tool_results:
|
|
227
|
+
self.save_tool_message(tool_call_id, result_content, tool_name)
|
|
228
|
+
|
|
229
|
+
console.print()
|
|
230
|
+
|
|
231
|
+
async def handle_command(self, cmd: str) -> None:
|
|
232
|
+
"""Handle slash commands."""
|
|
233
|
+
cmd = cmd.strip()
|
|
234
|
+
|
|
235
|
+
if cmd in ("/exit", "/quit"):
|
|
236
|
+
raise SystemExit(0)
|
|
237
|
+
elif cmd == "/help":
|
|
238
|
+
display_help()
|
|
239
|
+
elif cmd == "/clear":
|
|
240
|
+
clear_screen()
|
|
241
|
+
elif cmd == "/history":
|
|
242
|
+
messages = self.history_manager.get_messages(self.thread_id)
|
|
243
|
+
display_messages(messages)
|
|
244
|
+
elif cmd == "/model":
|
|
245
|
+
console.print(f"[cyan]Current model: {self.model}[/cyan]")
|
|
246
|
+
elif cmd == "/thread":
|
|
247
|
+
if self.thread:
|
|
248
|
+
console.print(f"[cyan]Thread ID: {self.thread.thread_id}[/cyan]")
|
|
249
|
+
console.print(f"[cyan]Name: {self.thread.name or 'Unnamed'}[/cyan]")
|
|
250
|
+
else:
|
|
251
|
+
console.print(f"[cyan]Thread ID: {self.thread_id}[/cyan]")
|
|
252
|
+
elif cmd == "/logs":
|
|
253
|
+
display_logs()
|
|
254
|
+
elif cmd == "/tools":
|
|
255
|
+
await display_tools_async(self.orchestrator.tool_manager)
|
|
256
|
+
elif cmd == "/context":
|
|
257
|
+
history = self.get_conversation_history()
|
|
258
|
+
status = self.orchestrator.get_context_status(history)
|
|
259
|
+
console.print("[cyan]Context Window Status:[/cyan]")
|
|
260
|
+
console.print(f" Tokens: {status['token_count']:,} / {status['effective_limit']:,}")
|
|
261
|
+
console.print(f" Used: {status['percentage_used']}%")
|
|
262
|
+
console.print(f" Messages: {status['message_count']}")
|
|
263
|
+
console.print(f" Needs compaction: {status['needs_compaction']}")
|
|
264
|
+
|
|
265
|
+
tool_summary = self.orchestrator.get_tool_outputs_summary()
|
|
266
|
+
if tool_summary["total_outputs"] > 0:
|
|
267
|
+
console.print("\n[cyan]Tool Outputs:[/cyan]")
|
|
268
|
+
console.print(f" Total outputs: {tool_summary['total_outputs']}")
|
|
269
|
+
console.print(f" Unique tools: {tool_summary['unique_tools']}")
|
|
270
|
+
console.print(f" Estimated tokens: {tool_summary['estimated_tokens']:,}")
|
|
271
|
+
elif cmd == "/pwd":
|
|
272
|
+
console.print(f"[cyan]{os.getcwd()}[/cyan]")
|
|
273
|
+
elif cmd.startswith("/cd "):
|
|
274
|
+
path = cmd[4:].strip()
|
|
275
|
+
try:
|
|
276
|
+
os.chdir(path)
|
|
277
|
+
console.print(f"[green]Changed to: {os.getcwd()}[/green]")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
280
|
+
elif cmd.startswith("/! "):
|
|
281
|
+
import subprocess
|
|
282
|
+
|
|
283
|
+
shell_cmd = cmd[3:].strip()
|
|
284
|
+
try:
|
|
285
|
+
result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True)
|
|
286
|
+
if result.stdout:
|
|
287
|
+
console.print(result.stdout)
|
|
288
|
+
if result.stderr:
|
|
289
|
+
console.print(f"[red]{result.stderr}[/red]")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
292
|
+
else:
|
|
293
|
+
console.print(f"[yellow]Unknown command: {cmd}[/yellow]")
|
|
294
|
+
console.print("[dim]Type /help for available commands[/dim]")
|
|
295
|
+
|
|
296
|
+
def cancel_current_task(self) -> None:
|
|
297
|
+
"""Cancel the current running task."""
|
|
298
|
+
if self.current_task and not self.current_task.done():
|
|
299
|
+
self.current_task.cancel()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def run_session(session: ChatSession) -> None:
|
|
303
|
+
"""Run the chat session loop."""
|
|
304
|
+
loop = asyncio.get_running_loop()
|
|
305
|
+
stop_requested = False
|
|
306
|
+
|
|
307
|
+
def handle_sigint() -> None:
|
|
308
|
+
"""Handle Ctrl+C - exit immediately."""
|
|
309
|
+
nonlocal stop_requested
|
|
310
|
+
if stop_requested or session._ctrl_c_count >= 1:
|
|
311
|
+
console.print("\n[bold red]Exiting.[/bold red]")
|
|
312
|
+
os._exit(0)
|
|
313
|
+
session._ctrl_c_count += 1
|
|
314
|
+
stop_requested = True
|
|
315
|
+
session.cancel_current_task()
|
|
316
|
+
console.print("\n[yellow]Exiting... (Ctrl+C again to force)[/yellow]")
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
loop.add_signal_handler(signal.SIGINT, handle_sigint)
|
|
320
|
+
except (NotImplementedError, ValueError):
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
# Display header
|
|
324
|
+
branch_name, _ = get_current_branch_name()
|
|
325
|
+
display_git_branch_info(branch_name)
|
|
326
|
+
|
|
327
|
+
thread_name = session.thread.name if session.thread else session.thread_id[:8]
|
|
328
|
+
display_thread_header(thread_name)
|
|
329
|
+
|
|
330
|
+
# Display existing messages
|
|
331
|
+
messages = session.history_manager.get_messages(session.thread_id)
|
|
332
|
+
if messages:
|
|
333
|
+
display_messages(messages)
|
|
334
|
+
|
|
335
|
+
# Main loop
|
|
336
|
+
while not stop_requested:
|
|
337
|
+
try:
|
|
338
|
+
session._ctrl_c_count = 0
|
|
339
|
+
user_input = await loop.run_in_executor(None, lambda: input("> "))
|
|
340
|
+
|
|
341
|
+
if stop_requested:
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
if not user_input.strip():
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
session.current_task = asyncio.create_task(session.process_input(user_input.strip()))
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
await session.current_task
|
|
351
|
+
except asyncio.CancelledError:
|
|
352
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
353
|
+
if stop_requested:
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
except EOFError:
|
|
357
|
+
console.print("\n[dim]Exiting.[/dim]")
|
|
358
|
+
break
|
|
359
|
+
except KeyboardInterrupt:
|
|
360
|
+
console.print("\n[dim]Exiting.[/dim]")
|
|
361
|
+
break
|
|
362
|
+
except SystemExit:
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
console.print("[dim]Session ended.[/dim]")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main(
|
|
369
|
+
thread_id_to_run: str,
|
|
370
|
+
model: str = DEFAULT_AI_MODEL,
|
|
371
|
+
stream: bool = False,
|
|
372
|
+
system_prompt: str | None = None,
|
|
373
|
+
target_symbol: str | None = None,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Main entry point for the TUI."""
|
|
376
|
+
try:
|
|
377
|
+
session = ChatSession(thread_id=thread_id_to_run, model=model, system_prompt=system_prompt)
|
|
378
|
+
asyncio.run(run_session(session))
|
|
379
|
+
except AuthError as e:
|
|
380
|
+
console.print(f"[red]Authentication error: {e}[/red]")
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
log.error(f"Fatal error: {e}", exc_info=True)
|
|
384
|
+
console.print(f"[red]Fatal error: {e}[/red]")
|
|
385
|
+
sys.exit(1)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def _run_single_message(message: str, model: str = DEFAULT_AI_MODEL) -> int:
|
|
389
|
+
"""Run a single message through the orchestrator.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
message: The user message to process
|
|
393
|
+
model: Model to use
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Exit code (0=success, 1=error, 2=needs interactive)
|
|
397
|
+
"""
|
|
398
|
+
orchestrator = create_orchestrator(default_model=model)
|
|
399
|
+
context = ExecutionContext(task=message, conversation_history=[])
|
|
400
|
+
|
|
401
|
+
accumulated_content = ""
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
# Status to stderr
|
|
405
|
+
print("\033[2m\033[3m[Processing...]\033[0m", file=sys.stderr)
|
|
406
|
+
|
|
407
|
+
async for event in orchestrator.stream(context):
|
|
408
|
+
if event.type == ExecutionEventType.CONTENT:
|
|
409
|
+
chunk = event.content or ""
|
|
410
|
+
cleaned = strip_control_signals(chunk, strip_whitespace=False)
|
|
411
|
+
accumulated_content += cleaned
|
|
412
|
+
|
|
413
|
+
elif event.type == ExecutionEventType.TOOL_CALL:
|
|
414
|
+
tc = event.metadata.get("tool_call", {})
|
|
415
|
+
func = tc.get("function", {})
|
|
416
|
+
tool_name = func.get("name", "unknown")
|
|
417
|
+
print(f"\033[2m\033[3m[Calling: {tool_name}]\033[0m", file=sys.stderr)
|
|
418
|
+
|
|
419
|
+
elif event.type == ExecutionEventType.TOOL_RESULT:
|
|
420
|
+
tool_name = event.metadata.get("tool_name", "unknown")
|
|
421
|
+
result = event.content or ""
|
|
422
|
+
preview = result[:80] + "..." if len(result) > 80 else result
|
|
423
|
+
preview = preview.replace("\n", " ")
|
|
424
|
+
print(
|
|
425
|
+
f"\033[2m\033[3m[Result: {tool_name}] {preview}\033[0m",
|
|
426
|
+
file=sys.stderr,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Final output to stdout
|
|
430
|
+
final_output = strip_control_signals(accumulated_content, strip_whitespace=True)
|
|
431
|
+
if final_output:
|
|
432
|
+
print(final_output)
|
|
433
|
+
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
except AuthError:
|
|
437
|
+
print("\033[2m\033[3m[Authentication required]\033[0m", file=sys.stderr)
|
|
438
|
+
return 2
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
441
|
+
log.error(f"Single message error: {e}", exc_info=True)
|
|
442
|
+
return 1
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def run_single_message_mode(message: str, model: str = DEFAULT_AI_MODEL) -> int:
|
|
446
|
+
"""Entry point for single message mode.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
message: The user message to process
|
|
450
|
+
model: Model to use
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Exit code (0=success, 1=error, 2=needs interactive)
|
|
454
|
+
"""
|
|
455
|
+
return asyncio.run(_run_single_message(message, model))
|
cadecoder/ui/state.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""State management for the TUI application."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TuiState:
|
|
9
|
+
"""Main TUI application state."""
|
|
10
|
+
|
|
11
|
+
chat_mode: str = "agent"
|
|
12
|
+
show_tools: bool = False
|
|
13
|
+
orchestrator: Any = None
|
|
14
|
+
message_history_index: int = -1
|
|
15
|
+
cached_user_messages: list[str] = field(default_factory=list)
|
|
16
|
+
expand_tool_results: bool = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Global application state
|
|
20
|
+
app_state = TuiState()
|