tarang 4.4.0__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.
- tarang/__init__.py +23 -0
- tarang/cli.py +1168 -0
- tarang/client/__init__.py +19 -0
- tarang/client/api_client.py +701 -0
- tarang/client/auth.py +178 -0
- tarang/context/__init__.py +41 -0
- tarang/context/bm25.py +218 -0
- tarang/context/chunker.py +984 -0
- tarang/context/graph.py +464 -0
- tarang/context/indexer.py +514 -0
- tarang/context/retriever.py +270 -0
- tarang/context/skeleton.py +282 -0
- tarang/context_collector.py +449 -0
- tarang/executor/__init__.py +6 -0
- tarang/executor/diff_apply.py +246 -0
- tarang/executor/linter.py +184 -0
- tarang/stream.py +1346 -0
- tarang/ui/__init__.py +7 -0
- tarang/ui/console.py +407 -0
- tarang/ui/diff_viewer.py +146 -0
- tarang/ui/formatter.py +1151 -0
- tarang/ui/keyboard.py +197 -0
- tarang/ws/__init__.py +14 -0
- tarang/ws/client.py +464 -0
- tarang/ws/executor.py +638 -0
- tarang/ws/handlers.py +590 -0
- tarang-4.4.0.dist-info/METADATA +102 -0
- tarang-4.4.0.dist-info/RECORD +31 -0
- tarang-4.4.0.dist-info/WHEEL +5 -0
- tarang-4.4.0.dist-info/entry_points.txt +2 -0
- tarang-4.4.0.dist-info/top_level.txt +1 -0
tarang/ws/handlers.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message Handlers for WebSocket Events.
|
|
3
|
+
|
|
4
|
+
Handles different event types from the backend:
|
|
5
|
+
- UI updates (thinking, progress, milestones)
|
|
6
|
+
- Tool requests and approvals
|
|
7
|
+
- Completion and errors
|
|
8
|
+
|
|
9
|
+
Integrates with Rich console for beautiful output.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from rich.console import Console, Group
|
|
18
|
+
from rich.live import Live
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskID, TimeElapsedColumn
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.syntax import Syntax
|
|
23
|
+
|
|
24
|
+
from tarang.ws.client import EventType, WSEvent
|
|
25
|
+
from tarang.ws.executor import ToolExecutor
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ExecutionState:
|
|
32
|
+
"""Tracks execution state for UI."""
|
|
33
|
+
current_phase: int = 0
|
|
34
|
+
total_phases: int = 0
|
|
35
|
+
phase_name: str = ""
|
|
36
|
+
milestones: List[str] = field(default_factory=list)
|
|
37
|
+
completed_milestones: List[str] = field(default_factory=list)
|
|
38
|
+
in_progress_milestone: str = ""
|
|
39
|
+
files_changed: List[str] = field(default_factory=list)
|
|
40
|
+
error: Optional[str] = None
|
|
41
|
+
job_id: Optional[str] = None
|
|
42
|
+
thinking_message: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Type for approval UI callback
|
|
46
|
+
ApprovalUICallback = Callable[[str, str, Dict[str, Any]], bool]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MessageHandlers:
|
|
50
|
+
"""
|
|
51
|
+
Handles WebSocket messages and updates UI.
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
handlers = MessageHandlers(
|
|
55
|
+
console=console,
|
|
56
|
+
executor=executor,
|
|
57
|
+
on_approval=lambda tool, desc, args: ui.confirm(desc),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
async for event in ws_client.execute(instruction, cwd):
|
|
61
|
+
await handlers.handle(event, ws_client)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
console: Console,
|
|
67
|
+
executor: ToolExecutor,
|
|
68
|
+
on_approval: Optional[ApprovalUICallback] = None,
|
|
69
|
+
verbose: bool = False,
|
|
70
|
+
auto_approve: bool = False,
|
|
71
|
+
):
|
|
72
|
+
self.console = console
|
|
73
|
+
self.executor = executor
|
|
74
|
+
self.on_approval = on_approval
|
|
75
|
+
self.verbose = verbose
|
|
76
|
+
self.auto_approve = auto_approve
|
|
77
|
+
|
|
78
|
+
self.state = ExecutionState()
|
|
79
|
+
self._progress: Optional[Progress] = None
|
|
80
|
+
self._phase_task_id: Optional[TaskID] = None
|
|
81
|
+
self._milestone_task_id: Optional[TaskID] = None
|
|
82
|
+
self._live: Optional[Live] = None
|
|
83
|
+
|
|
84
|
+
def _create_progress_display(self) -> Progress:
|
|
85
|
+
"""Create a progress display with phase and milestone tracking."""
|
|
86
|
+
return Progress(
|
|
87
|
+
SpinnerColumn(),
|
|
88
|
+
TextColumn("[bold blue]{task.fields[phase_name]}[/bold blue]"),
|
|
89
|
+
BarColumn(bar_width=30),
|
|
90
|
+
TextColumn("{task.percentage:.0f}%"),
|
|
91
|
+
TextColumn("[dim]{task.fields[milestone]}[/dim]"),
|
|
92
|
+
TimeElapsedColumn(),
|
|
93
|
+
console=self.console,
|
|
94
|
+
transient=False,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _build_status_panel(self) -> Panel:
|
|
98
|
+
"""Build a status panel showing current progress."""
|
|
99
|
+
if not self.state.phase_name:
|
|
100
|
+
return Panel(
|
|
101
|
+
f"[dim cyan]{self.state.thinking_message or 'Initializing...'}[/dim cyan]",
|
|
102
|
+
title="[bold] Status[/bold]",
|
|
103
|
+
border_style="blue",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Build milestone list with checkboxes
|
|
107
|
+
milestone_lines = []
|
|
108
|
+
for m in self.state.milestones:
|
|
109
|
+
if m in self.state.completed_milestones:
|
|
110
|
+
milestone_lines.append(f" [green][/green] {m}")
|
|
111
|
+
elif m == self.state.in_progress_milestone:
|
|
112
|
+
milestone_lines.append(f" [yellow][/yellow] {m}...")
|
|
113
|
+
else:
|
|
114
|
+
milestone_lines.append(f" [dim][ ][/dim] {m}")
|
|
115
|
+
|
|
116
|
+
phase_progress = f"Phase {self.state.current_phase}/{self.state.total_phases}"
|
|
117
|
+
completed = len(self.state.completed_milestones)
|
|
118
|
+
total = len(self.state.milestones)
|
|
119
|
+
|
|
120
|
+
content = f"[bold]{self.state.phase_name}[/bold] ({phase_progress})\n"
|
|
121
|
+
content += "\n".join(milestone_lines) if milestone_lines else ""
|
|
122
|
+
|
|
123
|
+
if self.state.files_changed:
|
|
124
|
+
content += f"\n\n[dim]Files: {len(self.state.files_changed)}[/dim]"
|
|
125
|
+
|
|
126
|
+
return Panel(
|
|
127
|
+
content,
|
|
128
|
+
title=f"[bold blue] {self.state.phase_name}[/bold blue]",
|
|
129
|
+
border_style="blue",
|
|
130
|
+
subtitle=f"[dim]{completed}/{total} milestones[/dim]",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def handle(self, event: WSEvent, ws_client) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Handle a WebSocket event.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
event: The event to handle
|
|
139
|
+
ws_client: WebSocket client for sending responses
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if execution should continue, False to stop
|
|
143
|
+
"""
|
|
144
|
+
handler = getattr(self, f"_handle_{event.type.value}", None)
|
|
145
|
+
|
|
146
|
+
if handler:
|
|
147
|
+
return await handler(event, ws_client)
|
|
148
|
+
else:
|
|
149
|
+
if self.verbose:
|
|
150
|
+
logger.debug(f"Unhandled event type: {event.type}")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
async def _handle_connected(self, event: WSEvent, ws_client) -> bool:
|
|
154
|
+
"""Handle connection established."""
|
|
155
|
+
session_id = event.data.get("session_id", "")
|
|
156
|
+
if self.verbose:
|
|
157
|
+
self.console.print(f"[dim]Connected: {session_id}[/dim]")
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
async def _handle_thinking(self, event: WSEvent, ws_client) -> bool:
|
|
161
|
+
"""Handle thinking/processing status."""
|
|
162
|
+
message = event.data.get("message", "Thinking...")
|
|
163
|
+
self.state.thinking_message = message
|
|
164
|
+
self.console.print(f"[dim cyan]{message}[/dim cyan]")
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
async def _handle_phase_start(self, event: WSEvent, ws_client) -> bool:
|
|
168
|
+
"""Handle new phase starting."""
|
|
169
|
+
phase = event.data.get("phase", 0)
|
|
170
|
+
total = event.data.get("total_phases", 1)
|
|
171
|
+
name = event.data.get("name", "")
|
|
172
|
+
milestones = event.data.get("milestones", [])
|
|
173
|
+
|
|
174
|
+
self.state.current_phase = phase
|
|
175
|
+
self.state.total_phases = total
|
|
176
|
+
self.state.phase_name = name
|
|
177
|
+
self.state.milestones = milestones
|
|
178
|
+
self.state.completed_milestones = []
|
|
179
|
+
self.state.in_progress_milestone = ""
|
|
180
|
+
|
|
181
|
+
# Calculate overall progress
|
|
182
|
+
phases_done = phase - 1
|
|
183
|
+
progress_percent = int((phases_done / total) * 100) if total > 0 else 0
|
|
184
|
+
|
|
185
|
+
self.console.print()
|
|
186
|
+
|
|
187
|
+
# Draw progress bar
|
|
188
|
+
bar_width = 30
|
|
189
|
+
filled = int(bar_width * phases_done / total) if total > 0 else 0
|
|
190
|
+
bar = "" * filled + "" * (bar_width - filled)
|
|
191
|
+
|
|
192
|
+
self.console.print(
|
|
193
|
+
f"[bold blue]Phase {phase}/{total}[/bold blue] [dim]{bar}[/dim] {progress_percent}%"
|
|
194
|
+
)
|
|
195
|
+
self.console.print(
|
|
196
|
+
Panel(
|
|
197
|
+
f"[bold]{name}[/bold]",
|
|
198
|
+
border_style="blue",
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if milestones:
|
|
203
|
+
for m in milestones:
|
|
204
|
+
self.console.print(f" [dim][ ][/dim] {m}")
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
async def _handle_milestone_update(self, event: WSEvent, ws_client) -> bool:
|
|
209
|
+
"""Handle milestone status change."""
|
|
210
|
+
milestone = event.data.get("milestone", "")
|
|
211
|
+
status = event.data.get("status", "")
|
|
212
|
+
|
|
213
|
+
if status == "completed":
|
|
214
|
+
if milestone not in self.state.completed_milestones:
|
|
215
|
+
self.state.completed_milestones.append(milestone)
|
|
216
|
+
if self.state.in_progress_milestone == milestone:
|
|
217
|
+
self.state.in_progress_milestone = ""
|
|
218
|
+
self.console.print(f" [green][/green] {milestone}")
|
|
219
|
+
elif status == "in_progress":
|
|
220
|
+
self.state.in_progress_milestone = milestone
|
|
221
|
+
self.console.print(f" [yellow][/yellow] {milestone}...")
|
|
222
|
+
elif status == "failed":
|
|
223
|
+
self.state.in_progress_milestone = ""
|
|
224
|
+
self.console.print(f" [red][/red] {milestone}")
|
|
225
|
+
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
async def _handle_progress(self, event: WSEvent, ws_client) -> bool:
|
|
229
|
+
"""Handle progress update."""
|
|
230
|
+
percent = event.data.get("percent", 0)
|
|
231
|
+
message = event.data.get("message", "")
|
|
232
|
+
phase = event.data.get("phase", 0)
|
|
233
|
+
total = event.data.get("total_phases", 1)
|
|
234
|
+
|
|
235
|
+
if self.verbose:
|
|
236
|
+
self.console.print(
|
|
237
|
+
f"[dim]Progress: {percent}% - {message}[/dim]"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
async def _handle_tool_request(self, event: WSEvent, ws_client) -> bool:
|
|
243
|
+
"""Handle tool execution request from backend."""
|
|
244
|
+
request_id = event.request_id or event.data.get("request_id", "")
|
|
245
|
+
tool = event.data.get("tool", "")
|
|
246
|
+
args = event.data.get("args", {})
|
|
247
|
+
|
|
248
|
+
# Show tool call with icon
|
|
249
|
+
tool_icons = {
|
|
250
|
+
"read_file": "",
|
|
251
|
+
"list_files": "",
|
|
252
|
+
"search_files": "",
|
|
253
|
+
"write_file": "",
|
|
254
|
+
"edit_file": "",
|
|
255
|
+
"delete_file": "",
|
|
256
|
+
"shell": "",
|
|
257
|
+
"get_file_info": "",
|
|
258
|
+
}
|
|
259
|
+
icon = tool_icons.get(tool, "")
|
|
260
|
+
|
|
261
|
+
# Build display info
|
|
262
|
+
if tool == "read_file":
|
|
263
|
+
display = f"{args.get('file_path', '')}"
|
|
264
|
+
elif tool == "list_files":
|
|
265
|
+
path = args.get('path', '.')
|
|
266
|
+
pattern = args.get('pattern', '')
|
|
267
|
+
display = f"{path}" + (f" ({pattern})" if pattern else "")
|
|
268
|
+
elif tool == "search_files":
|
|
269
|
+
display = f"'{args.get('pattern', '')}'"
|
|
270
|
+
elif tool == "write_file":
|
|
271
|
+
display = f"{args.get('file_path', '')}"
|
|
272
|
+
elif tool == "edit_file":
|
|
273
|
+
display = f"{args.get('file_path', '')}"
|
|
274
|
+
elif tool == "shell":
|
|
275
|
+
cmd = args.get('command', '')[:50]
|
|
276
|
+
display = f"`{cmd}`"
|
|
277
|
+
else:
|
|
278
|
+
display = ""
|
|
279
|
+
|
|
280
|
+
self.console.print(f" [dim cyan]{icon} {tool}[/dim cyan] {display}")
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
# Execute tool locally
|
|
284
|
+
result = await self.executor.execute(tool, args)
|
|
285
|
+
|
|
286
|
+
# Send result back
|
|
287
|
+
await ws_client.send_tool_result(request_id, result)
|
|
288
|
+
|
|
289
|
+
# Track file changes
|
|
290
|
+
if tool in ("write_file", "edit_file") and result.get("success"):
|
|
291
|
+
file_path = result.get("file_path", args.get("file_path", ""))
|
|
292
|
+
if file_path and file_path not in self.state.files_changed:
|
|
293
|
+
self.state.files_changed.append(file_path)
|
|
294
|
+
|
|
295
|
+
# Show result summary for verbose mode
|
|
296
|
+
if self.verbose:
|
|
297
|
+
if result.get("error"):
|
|
298
|
+
self.console.print(f" [red]Error: {result['error']}[/red]")
|
|
299
|
+
elif tool == "read_file":
|
|
300
|
+
lines = result.get("lines_returned", 0)
|
|
301
|
+
self.console.print(f" [dim]Read {lines} lines[/dim]")
|
|
302
|
+
elif tool == "list_files":
|
|
303
|
+
count = len(result.get("files", []))
|
|
304
|
+
self.console.print(f" [dim]Found {count} files[/dim]")
|
|
305
|
+
elif tool == "search_files":
|
|
306
|
+
count = result.get("total_matches", 0)
|
|
307
|
+
self.console.print(f" [dim]Found {count} matches[/dim]")
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.exception(f"Tool execution error: {tool}")
|
|
311
|
+
self.console.print(f" [red]Error: {e}[/red]")
|
|
312
|
+
await ws_client.send_tool_error(request_id, str(e))
|
|
313
|
+
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
async def _handle_approval_request(self, event: WSEvent, ws_client) -> bool:
|
|
317
|
+
"""Handle approval request for destructive operations."""
|
|
318
|
+
request_id = event.request_id or event.data.get("request_id", "")
|
|
319
|
+
tool = event.data.get("tool", "")
|
|
320
|
+
args = event.data.get("args", {})
|
|
321
|
+
description = event.data.get("description", "")
|
|
322
|
+
|
|
323
|
+
# Show what's being requested
|
|
324
|
+
self._show_approval_request(tool, args, description)
|
|
325
|
+
|
|
326
|
+
# Auto-approve if flag is set
|
|
327
|
+
if self.auto_approve:
|
|
328
|
+
approved = True
|
|
329
|
+
self.console.print("[dim green]Auto-approved[/dim green]")
|
|
330
|
+
elif self.on_approval:
|
|
331
|
+
approved = self.on_approval(tool, description, args)
|
|
332
|
+
else:
|
|
333
|
+
# Default: ask via console
|
|
334
|
+
self.console.print("[yellow]Approve this operation?[/yellow] (y/n): ", end="")
|
|
335
|
+
response = input().strip().lower()
|
|
336
|
+
approved = response in ("y", "yes")
|
|
337
|
+
|
|
338
|
+
if approved:
|
|
339
|
+
# Execute and send result
|
|
340
|
+
try:
|
|
341
|
+
result = await self.executor.execute(tool, args)
|
|
342
|
+
await ws_client.send_tool_result(request_id, result)
|
|
343
|
+
|
|
344
|
+
# Track file changes
|
|
345
|
+
if tool in ("write_file", "edit_file") and result.get("success"):
|
|
346
|
+
file_path = result.get("file_path", args.get("file_path", ""))
|
|
347
|
+
if file_path and file_path not in self.state.files_changed:
|
|
348
|
+
self.state.files_changed.append(file_path)
|
|
349
|
+
self.console.print(f" [green] Applied: {file_path}[/green]")
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
await ws_client.send_tool_error(request_id, str(e))
|
|
353
|
+
else:
|
|
354
|
+
# Send rejection
|
|
355
|
+
await ws_client.send_approval(request_id, False)
|
|
356
|
+
self.console.print(" [yellow] Skipped[/yellow]")
|
|
357
|
+
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
def _get_language_from_path(self, file_path: str) -> str:
|
|
361
|
+
"""Detect language from file extension for syntax highlighting."""
|
|
362
|
+
ext_map = {
|
|
363
|
+
".py": "python",
|
|
364
|
+
".js": "javascript",
|
|
365
|
+
".jsx": "jsx",
|
|
366
|
+
".ts": "typescript",
|
|
367
|
+
".tsx": "tsx",
|
|
368
|
+
".json": "json",
|
|
369
|
+
".yaml": "yaml",
|
|
370
|
+
".yml": "yaml",
|
|
371
|
+
".md": "markdown",
|
|
372
|
+
".html": "html",
|
|
373
|
+
".css": "css",
|
|
374
|
+
".scss": "scss",
|
|
375
|
+
".sql": "sql",
|
|
376
|
+
".sh": "bash",
|
|
377
|
+
".bash": "bash",
|
|
378
|
+
".zsh": "bash",
|
|
379
|
+
".go": "go",
|
|
380
|
+
".rs": "rust",
|
|
381
|
+
".rb": "ruby",
|
|
382
|
+
".java": "java",
|
|
383
|
+
".kt": "kotlin",
|
|
384
|
+
".swift": "swift",
|
|
385
|
+
".c": "c",
|
|
386
|
+
".cpp": "cpp",
|
|
387
|
+
".h": "c",
|
|
388
|
+
".hpp": "cpp",
|
|
389
|
+
}
|
|
390
|
+
import os
|
|
391
|
+
_, ext = os.path.splitext(file_path)
|
|
392
|
+
return ext_map.get(ext.lower(), "text")
|
|
393
|
+
|
|
394
|
+
def _show_approval_request(
|
|
395
|
+
self,
|
|
396
|
+
tool: str,
|
|
397
|
+
args: Dict[str, Any],
|
|
398
|
+
description: str,
|
|
399
|
+
):
|
|
400
|
+
"""Display approval request with details and syntax highlighting."""
|
|
401
|
+
self.console.print()
|
|
402
|
+
|
|
403
|
+
if tool == "write_file":
|
|
404
|
+
file_path = args.get("file_path", "")
|
|
405
|
+
content = args.get("content", "")
|
|
406
|
+
language = self._get_language_from_path(file_path)
|
|
407
|
+
|
|
408
|
+
self.console.print(f"[bold cyan]╭─ ✏️ Create: {file_path}[/bold cyan]")
|
|
409
|
+
if description:
|
|
410
|
+
self.console.print(f"[bold cyan]│[/bold cyan] [dim]{description}[/dim]")
|
|
411
|
+
|
|
412
|
+
# Show syntax-highlighted preview
|
|
413
|
+
lines = content.split("\n")
|
|
414
|
+
preview_lines = lines[:20]
|
|
415
|
+
preview = "\n".join(preview_lines)
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
syntax = Syntax(
|
|
419
|
+
preview,
|
|
420
|
+
language,
|
|
421
|
+
theme="monokai",
|
|
422
|
+
line_numbers=True,
|
|
423
|
+
word_wrap=True,
|
|
424
|
+
)
|
|
425
|
+
self.console.print(Panel(
|
|
426
|
+
syntax,
|
|
427
|
+
border_style="green",
|
|
428
|
+
title="[green]+ New File[/green]",
|
|
429
|
+
subtitle=f"[dim]{len(lines)} lines[/dim]" if len(lines) > 20 else None,
|
|
430
|
+
))
|
|
431
|
+
except Exception:
|
|
432
|
+
# Fallback to simple display
|
|
433
|
+
for line in preview_lines:
|
|
434
|
+
self.console.print(f" [green]+ {line}[/green]")
|
|
435
|
+
|
|
436
|
+
if len(lines) > 20:
|
|
437
|
+
self.console.print(f" [dim]... and {len(lines) - 20} more lines[/dim]")
|
|
438
|
+
|
|
439
|
+
elif tool == "edit_file":
|
|
440
|
+
file_path = args.get("file_path", "")
|
|
441
|
+
search = args.get("search", "")
|
|
442
|
+
replace = args.get("replace", "")
|
|
443
|
+
language = self._get_language_from_path(file_path)
|
|
444
|
+
|
|
445
|
+
self.console.print(f"[bold cyan]╭─ ✏️ Edit: {file_path}[/bold cyan]")
|
|
446
|
+
if description:
|
|
447
|
+
self.console.print(f"[bold cyan]│[/bold cyan] [dim]{description}[/dim]")
|
|
448
|
+
|
|
449
|
+
# Build unified diff display
|
|
450
|
+
search_lines = search.split("\n")
|
|
451
|
+
replace_lines = replace.split("\n")
|
|
452
|
+
|
|
453
|
+
# Show removal
|
|
454
|
+
if search_lines:
|
|
455
|
+
self.console.print("[bold cyan]│[/bold cyan]")
|
|
456
|
+
self.console.print("[bold cyan]│[/bold cyan] [red]Remove:[/red]")
|
|
457
|
+
for line in search_lines[:10]:
|
|
458
|
+
self.console.print(f"[bold cyan]│[/bold cyan] [red]- {line}[/red]")
|
|
459
|
+
if len(search_lines) > 10:
|
|
460
|
+
self.console.print(f"[bold cyan]│[/bold cyan] [dim]... ({len(search_lines)} lines total)[/dim]")
|
|
461
|
+
|
|
462
|
+
# Show addition
|
|
463
|
+
if replace_lines:
|
|
464
|
+
self.console.print("[bold cyan]│[/bold cyan]")
|
|
465
|
+
self.console.print("[bold cyan]│[/bold cyan] [green]Add:[/green]")
|
|
466
|
+
for line in replace_lines[:10]:
|
|
467
|
+
self.console.print(f"[bold cyan]│[/bold cyan] [green]+ {line}[/green]")
|
|
468
|
+
if len(replace_lines) > 10:
|
|
469
|
+
self.console.print(f"[bold cyan]│[/bold cyan] [dim]... ({len(replace_lines)} lines total)[/dim]")
|
|
470
|
+
|
|
471
|
+
self.console.print("[bold cyan]╰─[/bold cyan]")
|
|
472
|
+
|
|
473
|
+
elif tool == "delete_file":
|
|
474
|
+
file_path = args.get("file_path", "")
|
|
475
|
+
self.console.print(f"[bold red]╭─ 🗑️ Delete: {file_path}[/bold red]")
|
|
476
|
+
if description:
|
|
477
|
+
self.console.print(f"[bold red]│[/bold red] [dim]{description}[/dim]")
|
|
478
|
+
self.console.print("[bold red]╰─ This action cannot be undone![/bold red]")
|
|
479
|
+
|
|
480
|
+
elif tool == "shell":
|
|
481
|
+
command = args.get("command", "")
|
|
482
|
+
cwd = args.get("cwd", "")
|
|
483
|
+
timeout = args.get("timeout", 60)
|
|
484
|
+
|
|
485
|
+
self.console.print(f"[bold yellow]╭─ 💻 Shell Command[/bold yellow]")
|
|
486
|
+
if description:
|
|
487
|
+
self.console.print(f"[bold yellow]│[/bold yellow] [dim]{description}[/dim]")
|
|
488
|
+
self.console.print(f"[bold yellow]│[/bold yellow]")
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
syntax = Syntax(command, "bash", theme="monokai")
|
|
492
|
+
self.console.print(Panel(
|
|
493
|
+
syntax,
|
|
494
|
+
border_style="yellow",
|
|
495
|
+
title="[yellow]Command[/yellow]",
|
|
496
|
+
))
|
|
497
|
+
except Exception:
|
|
498
|
+
self.console.print(f"[bold yellow]│[/bold yellow] $ {command}")
|
|
499
|
+
|
|
500
|
+
if cwd:
|
|
501
|
+
self.console.print(f"[bold yellow]│[/bold yellow] [dim]Directory: {cwd}[/dim]")
|
|
502
|
+
self.console.print(f"[bold yellow]╰─[/bold yellow] [dim]Timeout: {timeout}s[/dim]")
|
|
503
|
+
|
|
504
|
+
else:
|
|
505
|
+
self.console.print(f"[bold]╭─ {tool}[/bold]")
|
|
506
|
+
if description:
|
|
507
|
+
self.console.print(f"[bold]│[/bold] [dim]{description}[/dim]")
|
|
508
|
+
self.console.print(f"[bold]╰─[/bold]")
|
|
509
|
+
|
|
510
|
+
async def _handle_complete(self, event: WSEvent, ws_client) -> bool:
|
|
511
|
+
"""Handle execution completed."""
|
|
512
|
+
summary = event.data.get("summary", "Completed")
|
|
513
|
+
files = event.data.get("files_changed", [])
|
|
514
|
+
phases = event.data.get("phases_completed", 0)
|
|
515
|
+
milestones = event.data.get("milestones_completed", 0)
|
|
516
|
+
|
|
517
|
+
self.console.print()
|
|
518
|
+
self.console.print(
|
|
519
|
+
Panel(
|
|
520
|
+
f"[green]{summary}[/green]\n\n"
|
|
521
|
+
f"[dim]Files changed: {len(files)}[/dim]\n"
|
|
522
|
+
f"[dim]Phases: {phases} | Milestones: {milestones}[/dim]",
|
|
523
|
+
title="[bold green] Complete[/bold green]",
|
|
524
|
+
border_style="green",
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
if files:
|
|
529
|
+
for f in files[:10]:
|
|
530
|
+
self.console.print(f" [dim]{f}[/dim]")
|
|
531
|
+
if len(files) > 10:
|
|
532
|
+
self.console.print(f" [dim]... and {len(files) - 10} more[/dim]")
|
|
533
|
+
|
|
534
|
+
return False # Stop iteration
|
|
535
|
+
|
|
536
|
+
async def _handle_error(self, event: WSEvent, ws_client) -> bool:
|
|
537
|
+
"""Handle error event."""
|
|
538
|
+
message = event.data.get("message", "Unknown error")
|
|
539
|
+
recoverable = event.data.get("recoverable", True)
|
|
540
|
+
|
|
541
|
+
self.state.error = message
|
|
542
|
+
|
|
543
|
+
self.console.print()
|
|
544
|
+
self.console.print(
|
|
545
|
+
Panel(
|
|
546
|
+
f"[red]{message}[/red]",
|
|
547
|
+
title="[bold red] Error[/bold red]",
|
|
548
|
+
border_style="red",
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return False # Stop iteration
|
|
553
|
+
|
|
554
|
+
async def _handle_paused(self, event: WSEvent, ws_client) -> bool:
|
|
555
|
+
"""Handle job paused (e.g., disconnect)."""
|
|
556
|
+
job_id = event.data.get("job_id", "")
|
|
557
|
+
resume_cmd = event.data.get("resume_command", "")
|
|
558
|
+
phase = event.data.get("phase", 0)
|
|
559
|
+
milestone = event.data.get("milestone", "")
|
|
560
|
+
|
|
561
|
+
self.console.print()
|
|
562
|
+
self.console.print(
|
|
563
|
+
Panel(
|
|
564
|
+
f"[yellow]Job paused at phase {phase}[/yellow]\n"
|
|
565
|
+
f"[dim]Milestone: {milestone}[/dim]\n\n"
|
|
566
|
+
f"[cyan]Resume with:[/cyan]\n"
|
|
567
|
+
f" {resume_cmd or f'tarang resume {job_id}'}",
|
|
568
|
+
title="[bold yellow] Paused[/bold yellow]",
|
|
569
|
+
border_style="yellow",
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
return False # Stop iteration
|
|
574
|
+
|
|
575
|
+
async def _handle_heartbeat(self, event: WSEvent, ws_client) -> bool:
|
|
576
|
+
"""Handle heartbeat - just acknowledge."""
|
|
577
|
+
return True
|
|
578
|
+
|
|
579
|
+
async def _handle_pong(self, event: WSEvent, ws_client) -> bool:
|
|
580
|
+
"""Handle pong response to our ping."""
|
|
581
|
+
return True
|
|
582
|
+
|
|
583
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
584
|
+
"""Get execution summary."""
|
|
585
|
+
return {
|
|
586
|
+
"files_changed": self.state.files_changed,
|
|
587
|
+
"phases_completed": self.state.current_phase,
|
|
588
|
+
"milestones_completed": len(self.state.completed_milestones),
|
|
589
|
+
"error": self.state.error,
|
|
590
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tarang
|
|
3
|
+
Version: 4.4.0
|
|
4
|
+
Summary: Tarang - AI Coding Agent (Hybrid WebSocket Architecture)
|
|
5
|
+
Author-email: Tarang Team <hello@devtarang.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://devtarang.ai
|
|
8
|
+
Project-URL: Documentation, https://docs.devtarang.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/tarang-ai/tarang-cli
|
|
10
|
+
Keywords: ai,coding,assistant,llm,developer-tools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: click>=8.0.0
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
26
|
+
Requires-Dist: rich>=13.0.0
|
|
27
|
+
Requires-Dist: websockets>=12.0
|
|
28
|
+
Requires-Dist: prompt_toolkit>=3.0.0
|
|
29
|
+
Requires-Dist: rank-bm25>=0.2.2
|
|
30
|
+
Requires-Dist: tree-sitter>=0.23.0
|
|
31
|
+
Requires-Dist: tree-sitter-python>=0.23.0
|
|
32
|
+
Requires-Dist: tree-sitter-javascript>=0.23.0
|
|
33
|
+
Requires-Dist: tree-sitter-sql>=0.3.0
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
39
|
+
|
|
40
|
+
# Tarang CLI
|
|
41
|
+
|
|
42
|
+
AI-powered coding assistant with ManagerAgent architecture.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install tarang
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Start interactive session
|
|
54
|
+
tarang run
|
|
55
|
+
|
|
56
|
+
# Run a single instruction
|
|
57
|
+
tarang run "create a hello world app"
|
|
58
|
+
|
|
59
|
+
# Run and exit
|
|
60
|
+
tarang run "fix linter errors" --once
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
- `tarang run [instruction]` - Start coding session (interactive or single)
|
|
66
|
+
- `tarang init <project>` - Initialize a new project
|
|
67
|
+
- `tarang chat` - Interactive chat mode
|
|
68
|
+
- `tarang status` - Show project status
|
|
69
|
+
- `tarang resume` - Resume interrupted execution
|
|
70
|
+
- `tarang reset` - Reset execution state
|
|
71
|
+
- `tarang clean` - Remove all Tarang state
|
|
72
|
+
- `tarang check` - Verify configuration
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
- `--project-dir, -p` - Project directory (default: current)
|
|
77
|
+
- `--config, -c` - Agent config (coder, explorer, orchestrator)
|
|
78
|
+
- `--verbose, -v` - Enable verbose output
|
|
79
|
+
- `--once` - Run single instruction and exit
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
Tarang requires an OpenRouter API key:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export OPENROUTER_API_KEY=your_key
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Project State
|
|
90
|
+
|
|
91
|
+
Tarang stores execution state in `.tarang/` directory:
|
|
92
|
+
- `state.json` - Current execution state
|
|
93
|
+
- Supports resume after interruption
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
- Website: [devtarang.ai](https://devtarang.ai)
|
|
98
|
+
- Documentation: [docs.devtarang.ai](https://docs.devtarang.ai)
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|