emdash-cli 0.1.30__py3-none-any.whl → 0.1.46__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.
- emdash_cli/__init__.py +15 -0
- emdash_cli/client.py +156 -0
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +53 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +41 -0
- emdash_cli/commands/agent/handlers/agents.py +421 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +200 -0
- emdash_cli/commands/agent/handlers/rules.py +394 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +582 -0
- emdash_cli/commands/agent/handlers/skills.py +440 -0
- emdash_cli/commands/agent/handlers/todos.py +98 -0
- emdash_cli/commands/agent/handlers/verify.py +648 -0
- emdash_cli/commands/agent/interactive.py +657 -0
- emdash_cli/commands/agent/menus.py +728 -0
- emdash_cli/commands/agent.py +7 -856
- emdash_cli/commands/server.py +99 -40
- emdash_cli/server_manager.py +70 -10
- emdash_cli/session_store.py +321 -0
- emdash_cli/sse_renderer.py +256 -110
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
- emdash_cli-0.1.46.dist-info/RECORD +49 -0
- emdash_cli-0.1.30.dist-info/RECORD +0 -29
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
emdash_cli/sse_renderer.py
CHANGED
|
@@ -43,6 +43,7 @@ class SSERenderer:
|
|
|
43
43
|
self._spec = None
|
|
44
44
|
self._spec_submitted = False
|
|
45
45
|
self._plan_submitted = None # Plan data when submit_plan tool is called
|
|
46
|
+
self._plan_mode_requested = None # Plan mode request data
|
|
46
47
|
self._pending_clarification = None
|
|
47
48
|
|
|
48
49
|
# Live display state
|
|
@@ -55,6 +56,8 @@ class SSERenderer:
|
|
|
55
56
|
# Sub-agent state (for inline updates)
|
|
56
57
|
self._subagent_tool_count = 0
|
|
57
58
|
self._subagent_current_tool = None
|
|
59
|
+
self._subagent_type = None
|
|
60
|
+
self._in_subagent_mode = False # Track when sub-agent is running
|
|
58
61
|
|
|
59
62
|
# Spinner animation thread
|
|
60
63
|
self._spinner_thread: Optional[threading.Thread] = None
|
|
@@ -65,6 +68,9 @@ class SSERenderer:
|
|
|
65
68
|
# Extended thinking storage
|
|
66
69
|
self._last_thinking: Optional[str] = None
|
|
67
70
|
|
|
71
|
+
# Context frame storage (rendered at end of stream)
|
|
72
|
+
self._last_context_frame: Optional[dict] = None
|
|
73
|
+
|
|
68
74
|
def render_stream(
|
|
69
75
|
self,
|
|
70
76
|
lines: Iterator[str],
|
|
@@ -83,6 +89,9 @@ class SSERenderer:
|
|
|
83
89
|
final_response = ""
|
|
84
90
|
interrupted = False
|
|
85
91
|
self._last_thinking = None # Reset thinking storage
|
|
92
|
+
self._pending_clarification = None # Reset clarification state
|
|
93
|
+
self._plan_submitted = None # Reset plan state
|
|
94
|
+
self._plan_mode_requested = None # Reset plan mode request state
|
|
86
95
|
|
|
87
96
|
# Start spinner while waiting for first event
|
|
88
97
|
if self.verbose:
|
|
@@ -121,12 +130,18 @@ class SSERenderer:
|
|
|
121
130
|
# Always stop spinner when stream ends
|
|
122
131
|
self._stop_spinner()
|
|
123
132
|
|
|
133
|
+
# Render context frame at the end (only once)
|
|
134
|
+
if self.verbose:
|
|
135
|
+
self._render_final_context_frame()
|
|
136
|
+
# Keep _last_context_frame for /context command access
|
|
137
|
+
|
|
124
138
|
return {
|
|
125
139
|
"content": final_response,
|
|
126
140
|
"session_id": self._session_id,
|
|
127
141
|
"spec": self._spec,
|
|
128
142
|
"spec_submitted": self._spec_submitted,
|
|
129
143
|
"plan_submitted": self._plan_submitted,
|
|
144
|
+
"plan_mode_requested": self._plan_mode_requested,
|
|
130
145
|
"clarification": self._pending_clarification,
|
|
131
146
|
"interrupted": interrupted,
|
|
132
147
|
"thinking": self._last_thinking,
|
|
@@ -183,8 +198,10 @@ class SSERenderer:
|
|
|
183
198
|
if not isinstance(data, dict):
|
|
184
199
|
data = {}
|
|
185
200
|
|
|
186
|
-
# Clear waiting indicator when new event arrives
|
|
187
|
-
|
|
201
|
+
# Clear waiting indicator when new event arrives (but not for sub-agent events)
|
|
202
|
+
subagent_id = data.get("subagent_id") if isinstance(data, dict) else None
|
|
203
|
+
if not subagent_id and not self._in_subagent_mode:
|
|
204
|
+
self._clear_waiting()
|
|
188
205
|
|
|
189
206
|
if event_type == "session_start":
|
|
190
207
|
self._render_session_start(data)
|
|
@@ -196,8 +213,14 @@ class SSERenderer:
|
|
|
196
213
|
self._waiting_for_next = True
|
|
197
214
|
if self.verbose:
|
|
198
215
|
self._start_spinner("thinking")
|
|
216
|
+
elif event_type == "subagent_start":
|
|
217
|
+
self._render_subagent_start(data)
|
|
218
|
+
elif event_type == "subagent_end":
|
|
219
|
+
self._render_subagent_end(data)
|
|
199
220
|
elif event_type == "thinking":
|
|
200
221
|
self._render_thinking(data)
|
|
222
|
+
elif event_type == "assistant_text":
|
|
223
|
+
self._render_assistant_text(data)
|
|
201
224
|
elif event_type == "progress":
|
|
202
225
|
self._render_progress(data)
|
|
203
226
|
elif event_type == "partial_response":
|
|
@@ -206,6 +229,8 @@ class SSERenderer:
|
|
|
206
229
|
return self._render_response(data)
|
|
207
230
|
elif event_type == "clarification":
|
|
208
231
|
self._render_clarification(data)
|
|
232
|
+
elif event_type == "plan_mode_requested":
|
|
233
|
+
self._render_plan_mode_requested(data)
|
|
209
234
|
elif event_type == "plan_submitted":
|
|
210
235
|
self._render_plan_submitted(data)
|
|
211
236
|
elif event_type == "error":
|
|
@@ -239,6 +264,9 @@ class SSERenderer:
|
|
|
239
264
|
self._tool_count = 0
|
|
240
265
|
self._completed_tools = []
|
|
241
266
|
|
|
267
|
+
# Start spinner while waiting for first tool
|
|
268
|
+
self._start_spinner("thinking")
|
|
269
|
+
|
|
242
270
|
def _render_tool_start(self, data: dict) -> None:
|
|
243
271
|
"""Render tool start event."""
|
|
244
272
|
if not self.verbose:
|
|
@@ -246,11 +274,22 @@ class SSERenderer:
|
|
|
246
274
|
|
|
247
275
|
name = data.get("name", "unknown")
|
|
248
276
|
args = data.get("args", {})
|
|
277
|
+
tool_id = data.get("tool_id")
|
|
249
278
|
subagent_id = data.get("subagent_id")
|
|
250
279
|
subagent_type = data.get("subagent_type")
|
|
251
280
|
|
|
252
281
|
self._tool_count += 1
|
|
253
|
-
|
|
282
|
+
|
|
283
|
+
# Store tool info for result rendering (keyed by tool_id for parallel support)
|
|
284
|
+
if not hasattr(self, '_pending_tools'):
|
|
285
|
+
self._pending_tools = {}
|
|
286
|
+
# Use tool_id if available, otherwise fall back to name
|
|
287
|
+
key = tool_id or name
|
|
288
|
+
self._pending_tools[key] = {"name": name, "args": args, "start_time": time.time(), "tool_id": tool_id}
|
|
289
|
+
self._current_tool = self._pending_tools[key]
|
|
290
|
+
|
|
291
|
+
# Stop spinner when tool starts
|
|
292
|
+
self._stop_spinner()
|
|
254
293
|
|
|
255
294
|
# Special handling for task tool (spawning sub-agents)
|
|
256
295
|
if name == "task":
|
|
@@ -264,59 +303,49 @@ class SSERenderer:
|
|
|
264
303
|
self._render_subagent_progress(subagent_type or "Agent", name, args)
|
|
265
304
|
return
|
|
266
305
|
|
|
267
|
-
#
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# Show spinner with tool name
|
|
271
|
-
spinner = SPINNER_FRAMES[0]
|
|
272
|
-
self.console.print(
|
|
273
|
-
f" [dim]┃[/dim] [yellow]{spinner}[/yellow] [bold]{name}[/bold] {args_summary}",
|
|
274
|
-
end="\r"
|
|
275
|
-
)
|
|
306
|
+
# Don't print anything here - wait for tool_result to print complete line
|
|
307
|
+
# This handles parallel tool execution correctly
|
|
276
308
|
|
|
277
309
|
def _render_subagent_progress(self, agent_type: str, tool_name: str, args: dict) -> None:
|
|
278
310
|
"""Render sub-agent progress on a single updating line."""
|
|
279
311
|
self._spinner_idx = (self._spinner_idx + 1) % len(SPINNER_FRAMES)
|
|
280
312
|
spinner = SPINNER_FRAMES[self._spinner_idx]
|
|
281
313
|
|
|
314
|
+
# Use stored type if not provided
|
|
315
|
+
agent_type = agent_type or self._subagent_type or "Agent"
|
|
316
|
+
|
|
282
317
|
# Get a short summary of what's being done
|
|
283
318
|
summary = ""
|
|
284
319
|
if "path" in args:
|
|
285
320
|
path = str(args["path"])
|
|
286
321
|
# Shorten long paths
|
|
287
|
-
if len(path) >
|
|
288
|
-
summary = "..." + path[-
|
|
322
|
+
if len(path) > 50:
|
|
323
|
+
summary = "..." + path[-47:]
|
|
289
324
|
else:
|
|
290
325
|
summary = path
|
|
291
326
|
elif "pattern" in args:
|
|
292
|
-
summary = str(args["pattern"])[:
|
|
327
|
+
summary = str(args["pattern"])[:50]
|
|
293
328
|
|
|
294
|
-
#
|
|
295
|
-
line = f"
|
|
296
|
-
#
|
|
297
|
-
sys.stdout.write(f"\r{
|
|
298
|
-
|
|
329
|
+
# Build compact progress line
|
|
330
|
+
line = f" │ {spinner} ({agent_type}) {self._subagent_tool_count} tools... {tool_name} {summary}"
|
|
331
|
+
# Use ANSI: move to column 0, clear line, print
|
|
332
|
+
sys.stdout.write(f"\r\033[K{line}")
|
|
333
|
+
sys.stdout.flush()
|
|
299
334
|
|
|
300
335
|
def _render_agent_spawn_start(self, args: dict) -> None:
|
|
301
|
-
"""
|
|
336
|
+
"""Track sub-agent spawn state (rendering done by subagent_start event)."""
|
|
302
337
|
agent_type = args.get("subagent_type", "Explore")
|
|
303
|
-
|
|
304
|
-
|
|
338
|
+
|
|
339
|
+
# Enter sub-agent mode: stop spinner, track state
|
|
340
|
+
self._stop_spinner()
|
|
341
|
+
self._in_subagent_mode = True
|
|
305
342
|
|
|
306
343
|
# Reset sub-agent tracking
|
|
307
344
|
self._subagent_tool_count = 0
|
|
308
345
|
self._subagent_current_tool = None
|
|
346
|
+
self._subagent_type = agent_type
|
|
309
347
|
|
|
310
|
-
#
|
|
311
|
-
prompt_display = prompt[:60] + "..." if len(prompt) > 60 else prompt
|
|
312
|
-
|
|
313
|
-
self.console.print()
|
|
314
|
-
self.console.print(
|
|
315
|
-
f" [bold magenta]◆ Spawning {agent_type} Agent[/bold magenta]"
|
|
316
|
-
)
|
|
317
|
-
if description:
|
|
318
|
-
self.console.print(f" [dim]{description}[/dim]")
|
|
319
|
-
self.console.print(f" [cyan]→[/cyan] {prompt_display}")
|
|
348
|
+
# Don't render here - subagent_start event will render the UI
|
|
320
349
|
|
|
321
350
|
def _render_tool_result(self, data: dict) -> None:
|
|
322
351
|
"""Render tool result event."""
|
|
@@ -342,31 +371,35 @@ class SSERenderer:
|
|
|
342
371
|
|
|
343
372
|
# Sub-agent events: don't print result lines, just keep updating progress
|
|
344
373
|
if subagent_id:
|
|
345
|
-
# Progress is already shown by _render_tool_start, nothing to do here
|
|
346
374
|
return
|
|
347
375
|
|
|
376
|
+
# Get tool info from pending tools (use tool_id if available)
|
|
377
|
+
pending_tools = getattr(self, '_pending_tools', {})
|
|
378
|
+
tool_id = data.get("tool_id")
|
|
379
|
+
key = tool_id or name
|
|
380
|
+
tool_info = pending_tools.pop(key, None) or self._current_tool or {}
|
|
381
|
+
args = tool_info.get("args", {})
|
|
382
|
+
|
|
348
383
|
# Calculate duration
|
|
349
384
|
duration = ""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
385
|
+
start_time = tool_info.get("start_time")
|
|
386
|
+
if start_time:
|
|
387
|
+
elapsed = time.time() - start_time
|
|
388
|
+
if elapsed >= 0.5: # Only show if >= 0.5s
|
|
389
|
+
duration = f" {elapsed:.1f}s"
|
|
354
390
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
args_summary = self._format_args_summary(self._current_tool.get("args", {}))
|
|
391
|
+
# Format args for display
|
|
392
|
+
args_display = self._format_tool_args(name, args)
|
|
358
393
|
|
|
394
|
+
# Build complete line: • ToolName(args)
|
|
359
395
|
if success:
|
|
360
|
-
|
|
361
|
-
result_text = f"[dim]{summary}[/dim]" if summary else ""
|
|
396
|
+
# Format: • tool(args) result 1.2s
|
|
397
|
+
result_text = f" [dim]{summary}[/dim]" if summary else ""
|
|
398
|
+
duration_text = f" [dim]{duration}[/dim]" if duration else ""
|
|
399
|
+
self.console.print(f" [green]✓[/green] [bold]{name}[/bold]({args_display}){result_text}{duration_text}")
|
|
362
400
|
else:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
# Overwrite the spinner line
|
|
367
|
-
self.console.print(
|
|
368
|
-
f" [dim]┃[/dim] {status_icon} [bold]{name}[/bold] {args_summary}{duration} {result_text}"
|
|
369
|
-
)
|
|
401
|
+
error_text = summary or "failed"
|
|
402
|
+
self.console.print(f" [red]✗[/red] [bold]{name}[/bold]({args_display}) [red]{error_text}[/red]")
|
|
370
403
|
|
|
371
404
|
self._completed_tools.append({
|
|
372
405
|
"name": name,
|
|
@@ -380,8 +413,11 @@ class SSERenderer:
|
|
|
380
413
|
success = data.get("success", True)
|
|
381
414
|
result_data = data.get("data") or {}
|
|
382
415
|
|
|
383
|
-
#
|
|
384
|
-
|
|
416
|
+
# Exit sub-agent mode
|
|
417
|
+
self._in_subagent_mode = False
|
|
418
|
+
|
|
419
|
+
# Clear the progress line and move to new line
|
|
420
|
+
sys.stdout.write(f"\r\033[K")
|
|
385
421
|
sys.stdout.flush()
|
|
386
422
|
|
|
387
423
|
# Calculate duration
|
|
@@ -415,6 +451,66 @@ class SSERenderer:
|
|
|
415
451
|
self.console.print()
|
|
416
452
|
self._current_tool = None
|
|
417
453
|
self._subagent_tool_count = 0
|
|
454
|
+
self._subagent_type = None
|
|
455
|
+
|
|
456
|
+
def _render_subagent_start(self, data: dict) -> None:
|
|
457
|
+
"""Render subagent start event - shows when Explore/Plan agent is spawned."""
|
|
458
|
+
agent_type = data.get("agent_type", "Agent")
|
|
459
|
+
prompt = data.get("prompt", "")
|
|
460
|
+
description = data.get("description", "")
|
|
461
|
+
|
|
462
|
+
# Stop any existing spinner
|
|
463
|
+
self._stop_spinner()
|
|
464
|
+
|
|
465
|
+
# Truncate prompt for display
|
|
466
|
+
prompt_display = prompt[:150] + "..." if len(prompt) > 150 else prompt
|
|
467
|
+
|
|
468
|
+
self.console.print()
|
|
469
|
+
# Use different colors for different agent types
|
|
470
|
+
if agent_type == "Plan":
|
|
471
|
+
self.console.print(f" [bold blue]◆ Spawning {agent_type} Agent[/bold blue]")
|
|
472
|
+
else:
|
|
473
|
+
self.console.print(f" [bold magenta]◆ Spawning {agent_type} Agent[/bold magenta]")
|
|
474
|
+
|
|
475
|
+
if description:
|
|
476
|
+
self.console.print(f" [dim]{description}[/dim]")
|
|
477
|
+
self.console.print(f" [cyan]→[/cyan] {prompt_display}")
|
|
478
|
+
|
|
479
|
+
# Enter subagent mode for tool tracking
|
|
480
|
+
self._in_subagent_mode = True
|
|
481
|
+
self._subagent_type = agent_type
|
|
482
|
+
self._subagent_tool_count = 0
|
|
483
|
+
self._subagent_start_time = time.time()
|
|
484
|
+
|
|
485
|
+
def _render_subagent_end(self, data: dict) -> None:
|
|
486
|
+
"""Render subagent end event - shows completion summary."""
|
|
487
|
+
agent_type = data.get("agent_type", "Agent")
|
|
488
|
+
success = data.get("success", True)
|
|
489
|
+
iterations = data.get("iterations", 0)
|
|
490
|
+
files_explored = data.get("files_explored", 0)
|
|
491
|
+
execution_time = data.get("execution_time", 0)
|
|
492
|
+
|
|
493
|
+
# Exit subagent mode
|
|
494
|
+
self._in_subagent_mode = False
|
|
495
|
+
|
|
496
|
+
if success:
|
|
497
|
+
self.console.print(
|
|
498
|
+
f" [green]✓[/green] {agent_type} completed [dim]({execution_time:.1f}s)[/dim]"
|
|
499
|
+
)
|
|
500
|
+
# Show stats
|
|
501
|
+
stats = []
|
|
502
|
+
if iterations > 0:
|
|
503
|
+
stats.append(f"{iterations} turns")
|
|
504
|
+
if files_explored > 0:
|
|
505
|
+
stats.append(f"{files_explored} files")
|
|
506
|
+
if stats:
|
|
507
|
+
self.console.print(f" [dim]{' · '.join(stats)}[/dim]")
|
|
508
|
+
else:
|
|
509
|
+
self.console.print(f" [red]✗[/red] {agent_type} failed")
|
|
510
|
+
|
|
511
|
+
self.console.print()
|
|
512
|
+
self._subagent_type = None
|
|
513
|
+
self._subagent_tool_count = 0
|
|
418
514
|
|
|
419
515
|
def _format_args_summary(self, args: dict) -> str:
|
|
420
516
|
"""Format args into a compact summary string."""
|
|
@@ -422,7 +518,7 @@ class SSERenderer:
|
|
|
422
518
|
return ""
|
|
423
519
|
|
|
424
520
|
parts = []
|
|
425
|
-
for
|
|
521
|
+
for _, v in list(args.items())[:2]:
|
|
426
522
|
v_str = str(v)
|
|
427
523
|
if len(v_str) > 40:
|
|
428
524
|
v_str = v_str[:37] + "..."
|
|
@@ -430,6 +526,41 @@ class SSERenderer:
|
|
|
430
526
|
|
|
431
527
|
return " ".join(parts)
|
|
432
528
|
|
|
529
|
+
def _format_tool_args(self, tool_name: str, args: dict) -> str:
|
|
530
|
+
"""Format tool args in Claude Code style: ToolName(key_arg_value).
|
|
531
|
+
|
|
532
|
+
Shows the most relevant arg for each tool type.
|
|
533
|
+
"""
|
|
534
|
+
if not args:
|
|
535
|
+
return ""
|
|
536
|
+
|
|
537
|
+
# Tool-specific formatting for cleaner display
|
|
538
|
+
if tool_name in ("glob", "grep", "semantic_search"):
|
|
539
|
+
pattern = args.get("pattern", args.get("query", ""))
|
|
540
|
+
if pattern:
|
|
541
|
+
return f'[dim]pattern:[/dim] "{pattern}"' if len(pattern) < 50 else f'[dim]pattern:[/dim] "{pattern[:47]}..."'
|
|
542
|
+
elif tool_name in ("read_file", "write_to_file", "list_files"):
|
|
543
|
+
path = args.get("path", "")
|
|
544
|
+
if path:
|
|
545
|
+
return f"[dim]{path}[/dim]"
|
|
546
|
+
elif tool_name == "bash":
|
|
547
|
+
cmd = args.get("command", "")
|
|
548
|
+
if cmd:
|
|
549
|
+
return f"[dim]{cmd[:60]}{'...' if len(cmd) > 60 else ''}[/dim]"
|
|
550
|
+
elif tool_name == "edit_file":
|
|
551
|
+
path = args.get("path", "")
|
|
552
|
+
if path:
|
|
553
|
+
return f"[dim]{path}[/dim]"
|
|
554
|
+
|
|
555
|
+
# Default: show first arg value
|
|
556
|
+
if args:
|
|
557
|
+
first_val = str(list(args.values())[0])
|
|
558
|
+
if len(first_val) > 50:
|
|
559
|
+
first_val = first_val[:47] + "..."
|
|
560
|
+
return f"[dim]{first_val}[/dim]"
|
|
561
|
+
|
|
562
|
+
return ""
|
|
563
|
+
|
|
433
564
|
def _render_thinking(self, data: dict) -> None:
|
|
434
565
|
"""Render thinking event.
|
|
435
566
|
|
|
@@ -442,15 +573,15 @@ class SSERenderer:
|
|
|
442
573
|
|
|
443
574
|
# Check if this is extended thinking (long content) vs short progress message
|
|
444
575
|
if len(message) > 200:
|
|
445
|
-
# Extended thinking - show
|
|
576
|
+
# Extended thinking - show full content
|
|
446
577
|
self._stop_spinner()
|
|
447
578
|
lines = message.strip().split("\n")
|
|
448
|
-
preview = lines[0][:80] + "..." if len(lines[0]) > 80 else lines[0]
|
|
449
579
|
line_count = len(lines)
|
|
450
580
|
char_count = len(message)
|
|
451
581
|
|
|
452
582
|
self.console.print(f" [dim]┃[/dim] [dim italic]💭 Thinking ({char_count:,} chars, {line_count} lines)[/dim italic]")
|
|
453
|
-
|
|
583
|
+
for line in lines:
|
|
584
|
+
self.console.print(f" [dim]┃[/dim] [dim] {line}[/dim]")
|
|
454
585
|
|
|
455
586
|
# Store thinking for potential later display
|
|
456
587
|
self._last_thinking = message
|
|
@@ -458,6 +589,24 @@ class SSERenderer:
|
|
|
458
589
|
# Short progress message
|
|
459
590
|
self.console.print(f" [dim]┃[/dim] [dim italic]💭 {message}[/dim italic]")
|
|
460
591
|
|
|
592
|
+
def _render_assistant_text(self, data: dict) -> None:
|
|
593
|
+
"""Render intermediate assistant text (between tool calls)."""
|
|
594
|
+
if not self.verbose:
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
content = data.get("content", "").strip()
|
|
598
|
+
if not content:
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
# Stop spinner while showing text
|
|
602
|
+
self._stop_spinner()
|
|
603
|
+
|
|
604
|
+
# Show as bullet point like Claude Code (cyan for assistant reasoning)
|
|
605
|
+
# Truncate long content
|
|
606
|
+
if len(content) > 200:
|
|
607
|
+
content = content[:197] + "..."
|
|
608
|
+
self.console.print(f" [cyan]•[/cyan] [italic]{content}[/italic]")
|
|
609
|
+
|
|
461
610
|
def _render_progress(self, data: dict) -> None:
|
|
462
611
|
"""Render progress event."""
|
|
463
612
|
if not self.verbose:
|
|
@@ -507,73 +656,47 @@ class SSERenderer:
|
|
|
507
656
|
self.console.print(f" [yellow][{i}][/yellow] {opt}")
|
|
508
657
|
self.console.print()
|
|
509
658
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
659
|
+
# Always store clarification (with or without options)
|
|
660
|
+
self._pending_clarification = {
|
|
661
|
+
"question": question,
|
|
662
|
+
"context": context,
|
|
663
|
+
"options": options,
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
def _render_plan_mode_requested(self, data: dict) -> None:
|
|
667
|
+
"""Render plan mode request event and store for menu display."""
|
|
668
|
+
from rich.panel import Panel
|
|
669
|
+
|
|
670
|
+
reason = data.get("reason", "")
|
|
671
|
+
|
|
672
|
+
# Store the request data for the CLI to show the menu
|
|
673
|
+
self._plan_mode_requested = data
|
|
674
|
+
|
|
675
|
+
# Display the request
|
|
676
|
+
self.console.print()
|
|
677
|
+
self.console.print(Panel(
|
|
678
|
+
f"[bold]Request to Enter Plan Mode[/bold]\n\n{reason}",
|
|
679
|
+
title="[yellow]⚡ Plan Mode Request[/yellow]",
|
|
680
|
+
border_style="yellow",
|
|
681
|
+
))
|
|
517
682
|
|
|
518
683
|
def _render_plan_submitted(self, data: dict) -> None:
|
|
519
684
|
"""Render plan submission event and store for menu display."""
|
|
520
685
|
from rich.panel import Panel
|
|
521
|
-
from rich.
|
|
522
|
-
from rich.text import Text
|
|
686
|
+
from rich.markdown import Markdown
|
|
523
687
|
|
|
524
|
-
|
|
525
|
-
summary = data.get("summary", "")
|
|
526
|
-
files_to_modify = data.get("files_to_modify", [])
|
|
527
|
-
implementation_steps = data.get("implementation_steps", [])
|
|
528
|
-
risks = data.get("risks", [])
|
|
529
|
-
testing_strategy = data.get("testing_strategy", "")
|
|
688
|
+
plan = data.get("plan", "")
|
|
530
689
|
|
|
531
690
|
# Store the plan data for the CLI to show the menu
|
|
532
691
|
self._plan_submitted = data
|
|
533
692
|
|
|
534
|
-
#
|
|
693
|
+
# Render plan as markdown in a panel
|
|
535
694
|
self.console.print()
|
|
536
695
|
self.console.print(Panel(
|
|
537
|
-
|
|
696
|
+
Markdown(plan),
|
|
538
697
|
title="[cyan]📋 Plan[/cyan]",
|
|
539
698
|
border_style="cyan",
|
|
540
699
|
))
|
|
541
|
-
|
|
542
|
-
# Critical Files table (always shown)
|
|
543
|
-
if files_to_modify:
|
|
544
|
-
files_table = Table(title="Critical Files", show_header=True, header_style="bold cyan")
|
|
545
|
-
files_table.add_column("File", style="yellow")
|
|
546
|
-
files_table.add_column("Lines", style="dim")
|
|
547
|
-
files_table.add_column("Changes", style="white")
|
|
548
|
-
|
|
549
|
-
for f in files_to_modify:
|
|
550
|
-
if isinstance(f, dict):
|
|
551
|
-
files_table.add_row(
|
|
552
|
-
f.get("path", ""),
|
|
553
|
-
f.get("lines", ""),
|
|
554
|
-
f.get("changes", "")
|
|
555
|
-
)
|
|
556
|
-
else:
|
|
557
|
-
files_table.add_row(str(f), "", "")
|
|
558
|
-
|
|
559
|
-
self.console.print(files_table)
|
|
560
|
-
|
|
561
|
-
# Implementation Steps (only if provided)
|
|
562
|
-
if implementation_steps:
|
|
563
|
-
self.console.print("\n[bold cyan]Implementation Steps[/bold cyan]")
|
|
564
|
-
for i, step in enumerate(implementation_steps, 1):
|
|
565
|
-
self.console.print(f" [dim]{i}.[/dim] {step}")
|
|
566
|
-
|
|
567
|
-
# Risks (only if provided)
|
|
568
|
-
if risks:
|
|
569
|
-
self.console.print("\n[bold yellow]⚠ Risks[/bold yellow]")
|
|
570
|
-
for risk in risks:
|
|
571
|
-
self.console.print(f" [yellow]•[/yellow] {risk}")
|
|
572
|
-
|
|
573
|
-
# Testing (only if provided)
|
|
574
|
-
if testing_strategy:
|
|
575
|
-
self.console.print(f"\n[bold green]Testing:[/bold green] {testing_strategy}")
|
|
576
|
-
|
|
577
700
|
self.console.print()
|
|
578
701
|
|
|
579
702
|
def _render_error(self, data: dict) -> None:
|
|
@@ -602,7 +725,16 @@ class SSERenderer:
|
|
|
602
725
|
self.console.print(f"\n[red]Session ended with error: {error}[/red]")
|
|
603
726
|
|
|
604
727
|
def _render_context_frame(self, data: dict) -> None:
|
|
605
|
-
"""
|
|
728
|
+
"""Store context frame data to render at end of stream."""
|
|
729
|
+
# Just store the latest context frame, will render at end
|
|
730
|
+
self._last_context_frame = data
|
|
731
|
+
|
|
732
|
+
def _render_final_context_frame(self) -> None:
|
|
733
|
+
"""Render the final context frame at end of agent loop."""
|
|
734
|
+
if not self._last_context_frame:
|
|
735
|
+
return
|
|
736
|
+
|
|
737
|
+
data = self._last_context_frame
|
|
606
738
|
adding = data.get("adding") or {}
|
|
607
739
|
reading = data.get("reading") or {}
|
|
608
740
|
|
|
@@ -646,3 +778,17 @@ class SSERenderer:
|
|
|
646
778
|
|
|
647
779
|
if stats:
|
|
648
780
|
self.console.print(f" [dim]{' · '.join(stats)}[/dim]")
|
|
781
|
+
|
|
782
|
+
# Show reranked items (for testing)
|
|
783
|
+
items = reading.get("items", [])
|
|
784
|
+
if items:
|
|
785
|
+
self.console.print(f"\n [bold]Reranked Items ({len(items)}):[/bold]")
|
|
786
|
+
for item in items[:10]: # Show top 10
|
|
787
|
+
name = item.get("name", "?")
|
|
788
|
+
item_type = item.get("type", "?")
|
|
789
|
+
score = item.get("score")
|
|
790
|
+
file_path = item.get("file", "")
|
|
791
|
+
score_str = f" [cyan]({score:.3f})[/cyan]" if score is not None else ""
|
|
792
|
+
self.console.print(f" [dim]{item_type}[/dim] [bold]{name}[/bold]{score_str}")
|
|
793
|
+
if file_path:
|
|
794
|
+
self.console.print(f" [dim]{file_path}[/dim]")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emdash-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.46
|
|
4
4
|
Summary: EmDash CLI - Command-line interface for code intelligence
|
|
5
5
|
Author: Em Dash Team
|
|
6
6
|
Requires-Python: >=3.10,<4.0
|
|
@@ -10,10 +10,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
-
Provides-Extra: images
|
|
14
13
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
15
|
-
Requires-Dist: emdash-core (>=0.1.
|
|
14
|
+
Requires-Dist: emdash-core (>=0.1.46)
|
|
16
15
|
Requires-Dist: httpx (>=0.25.0)
|
|
17
|
-
Requires-Dist: pillow (>=10.0.0) ; extra == "images"
|
|
18
16
|
Requires-Dist: prompt_toolkit (>=3.0.43,<4.0.0)
|
|
19
17
|
Requires-Dist: rich (>=13.7.0)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
emdash_cli/__init__.py,sha256=21JmQcWge3QOM4hLjtBdu-7AoxJonJuIwMeobWIjr2w,690
|
|
2
|
+
emdash_cli/client.py,sha256=00RTy6yAr1j2P0XIJGOhcjP5C16-5EGJLXBUKFUcxAU,22054
|
|
3
|
+
emdash_cli/clipboard.py,sha256=3iwkfj4Od1gPSsMbIBc6sx9XH2XCNgvc5Uu2pPRGcpw,2371
|
|
4
|
+
emdash_cli/commands/__init__.py,sha256=D9edXBHm69tueUtE4DggTA1_Yjsl9YZaKjBVDY2D_gQ,712
|
|
5
|
+
emdash_cli/commands/agent/__init__.py,sha256=Q02HtlODcid-YS_HDqBjbW6V8Xg9pYy0gWvrxn1EB9E,410
|
|
6
|
+
emdash_cli/commands/agent/cli.py,sha256=bfRSRAo6exy3llBoHS81AgGKP36xJ3MyL9eoLBtnJW4,2975
|
|
7
|
+
emdash_cli/commands/agent/constants.py,sha256=1H9fEObjsMeFzXVz0kQdB6HamSmvYqeMQLnX0ozStYU,1915
|
|
8
|
+
emdash_cli/commands/agent/file_utils.py,sha256=2YOKybPVGyjmImbqLHQOIL7zDKeVjQMWssAh9ZX1_Vc,5257
|
|
9
|
+
emdash_cli/commands/agent/handlers/__init__.py,sha256=70VjCycodve_zhQN_kuuYFY2Fi2vpvI8uawJqxXpCY8,958
|
|
10
|
+
emdash_cli/commands/agent/handlers/agents.py,sha256=5k_1lp_lynTrzqyYAPz9UXFt9esFzy6v0hOoyfeZCHI,14329
|
|
11
|
+
emdash_cli/commands/agent/handlers/auth.py,sha256=L2CrWdHg7Xb8e1l3UEGaYPmLubIx1r-e__qkzy5mQZ0,2401
|
|
12
|
+
emdash_cli/commands/agent/handlers/doctor.py,sha256=yECBNFgHzHS2m-P2xmXkZ4AKpTMhT2IYS-Dz4NcoM94,10019
|
|
13
|
+
emdash_cli/commands/agent/handlers/hooks.py,sha256=vZUdzppD6ztw8S757Rb6FMmWhU7n3SIayHHIIry41yU,5176
|
|
14
|
+
emdash_cli/commands/agent/handlers/mcp.py,sha256=ei76RZQ6bniaxxMX8yh9aIkKJET5aKFT7oj4GiPQ2wM,6255
|
|
15
|
+
emdash_cli/commands/agent/handlers/misc.py,sha256=q6zvqd0WzsZgUjVbXuxZVlFrLxi9Tq8Ll3ljhpQeWhY,7951
|
|
16
|
+
emdash_cli/commands/agent/handlers/rules.py,sha256=lvpBGmcSO0vfRCn052QMyH50GXMGhYlEFeJF6fNoZPg,12510
|
|
17
|
+
emdash_cli/commands/agent/handlers/sessions.py,sha256=GGDP9AsVYhbKT7HtDrGu-y2FsEyos39UqgOceebJLXs,6609
|
|
18
|
+
emdash_cli/commands/agent/handlers/setup.py,sha256=5EAXXb3aku86I1X8_eskn-5tTzxmlyyzljsDkVKlyEQ,17246
|
|
19
|
+
emdash_cli/commands/agent/handlers/skills.py,sha256=3a3ZW51WesfGd_V7JJwmW80gJhFiDVONJA6Yr8jRaQI,14538
|
|
20
|
+
emdash_cli/commands/agent/handlers/todos.py,sha256=N3BTxvOxRpVpgQGezE66kKWYKeYFrF9l-pKAE2V_4z4,3931
|
|
21
|
+
emdash_cli/commands/agent/handlers/verify.py,sha256=9F_pzRZyqUr9MvFQx7IxsajrKIrBwoRjXL5RWxi9OTo,20717
|
|
22
|
+
emdash_cli/commands/agent/interactive.py,sha256=6ItB_HnZteHN-5E-bfa0dMgRw2pyyKDtinT530qxe54,24597
|
|
23
|
+
emdash_cli/commands/agent/menus.py,sha256=Kvw142-pbv9pLRAzd1E3XvsjRRUhgNozCeBex9uavN0,21269
|
|
24
|
+
emdash_cli/commands/agent.py,sha256=fJmiBecmnjdaeNwHIpexC4O6sISIqVD8SKiySBoBWvY,321
|
|
25
|
+
emdash_cli/commands/analyze.py,sha256=c9ztbv0Ra7g2AlDmMOy-9L51fDVuoqbuzxnRfomoFIQ,4403
|
|
26
|
+
emdash_cli/commands/auth.py,sha256=SpWdqO1bJCgt4x1B4Pr7hNOucwTuBFJ1oGPOzXtvwZM,3816
|
|
27
|
+
emdash_cli/commands/db.py,sha256=nZK7gLDVE2lAQVYrMx6Swscml5OAtkbg-EcSNSvRIlA,2922
|
|
28
|
+
emdash_cli/commands/embed.py,sha256=kqP5jtYCsZ2_s_I1DjzIUgaod1VUvPiRO0jIIY0HtCs,3244
|
|
29
|
+
emdash_cli/commands/index.py,sha256=uFNC5whhU9JdF_59FeM99OPdzKLBTJLkLO6vp9pt944,6959
|
|
30
|
+
emdash_cli/commands/plan.py,sha256=BRiyIhfy_zz2PYy4Qo3a0t77GwHhdssZk6NImOkPi-w,2189
|
|
31
|
+
emdash_cli/commands/projectmd.py,sha256=4y4cn_yFw85jMUm52nGjpqnd-YWvs6ZNEMWJGeJC17Q,1605
|
|
32
|
+
emdash_cli/commands/research.py,sha256=xtI9_9emY7-rGQD5xJALTxtgTFmI4dplYW148dtTaTs,1553
|
|
33
|
+
emdash_cli/commands/rules.py,sha256=n85CCG0WNIBEsUK9STJetPmZxoypQtest5BGPsXl0ac,2712
|
|
34
|
+
emdash_cli/commands/search.py,sha256=DrSv_oN2xF1NaKCBICdyII7eupVRsDQ2ysW-TPSU0X0,1661
|
|
35
|
+
emdash_cli/commands/server.py,sha256=uqgp0DH7bJhu8E3k8PM1IaPLAogTtjCXu6iaZyfiOOw,5868
|
|
36
|
+
emdash_cli/commands/skills.py,sha256=8N4279Hr8u2L8AgVjSTRVBLJBcXhN5DN7dn5fME62bs,9989
|
|
37
|
+
emdash_cli/commands/spec.py,sha256=qafDmzKyRH035p3xTm_VTUsQLDZblIzIg-dxjEPv6tM,1494
|
|
38
|
+
emdash_cli/commands/swarm.py,sha256=s_cntuorNdtNNTD2Qs1p2IcHghMrBMOQuturPS3y9mM,2661
|
|
39
|
+
emdash_cli/commands/tasks.py,sha256=TdyunjSV5w7jpNFwv0fTL-_No5Fyvdm7Z2nXqxWSJec,1635
|
|
40
|
+
emdash_cli/commands/team.py,sha256=K1-IJg6iG-9HMF_3JmpNDlNs1PYbb-ThFHU9KU_jKRo,1430
|
|
41
|
+
emdash_cli/keyboard.py,sha256=haYYAuhYGtdjomzhIFy_3Z3eN3BXfMdb4uRQjwB0tbk,4593
|
|
42
|
+
emdash_cli/main.py,sha256=c-faWp-jzf9a0BbXhVoPvPQfGWSryXpYfswehqZCYPM,2593
|
|
43
|
+
emdash_cli/server_manager.py,sha256=saSxTaCu-b2n2-cIA3VzUe-Tj8ABpeZ39TPOdqjBzVI,9397
|
|
44
|
+
emdash_cli/session_store.py,sha256=GjS73GLSZ3oTNtrFHMcyiP6GnH0Dvfvs6r4s3-bfEaM,9424
|
|
45
|
+
emdash_cli/sse_renderer.py,sha256=kP_MygMQqW06kmPsD2BRMOdvXJTC4NG6_8_2IfioL4I,29092
|
|
46
|
+
emdash_cli-0.1.46.dist-info/METADATA,sha256=0-d6wf19RzND2n9QHDqQUPgROpQRglX0z7tTF4SvUw0,662
|
|
47
|
+
emdash_cli-0.1.46.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
48
|
+
emdash_cli-0.1.46.dist-info/entry_points.txt,sha256=31CuYD0k-tM8csFWDunc-JoZTxXaifj3oIXz4V0p6F0,122
|
|
49
|
+
emdash_cli-0.1.46.dist-info/RECORD,,
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
emdash_cli/__init__.py,sha256=Rnn2O7B8OCEKlVtNRbWOU2-GN75_KLmhEJgOZzY-KwE,232
|
|
2
|
-
emdash_cli/client.py,sha256=aQO_wF4XQaqig6RWhTA3FIJslweo7LsZHCk_GBVGdvw,17117
|
|
3
|
-
emdash_cli/clipboard.py,sha256=hcg5sbIhbixqzpJdonoFLGBlSo2AKjplNrWy5PGnqaY,3564
|
|
4
|
-
emdash_cli/commands/__init__.py,sha256=D9edXBHm69tueUtE4DggTA1_Yjsl9YZaKjBVDY2D_gQ,712
|
|
5
|
-
emdash_cli/commands/agent.py,sha256=dLNNyJNL9uzZcBapzFghdjb-Xa27PglpbFNAMPGa3qc,28772
|
|
6
|
-
emdash_cli/commands/analyze.py,sha256=c9ztbv0Ra7g2AlDmMOy-9L51fDVuoqbuzxnRfomoFIQ,4403
|
|
7
|
-
emdash_cli/commands/auth.py,sha256=SpWdqO1bJCgt4x1B4Pr7hNOucwTuBFJ1oGPOzXtvwZM,3816
|
|
8
|
-
emdash_cli/commands/db.py,sha256=nZK7gLDVE2lAQVYrMx6Swscml5OAtkbg-EcSNSvRIlA,2922
|
|
9
|
-
emdash_cli/commands/embed.py,sha256=kqP5jtYCsZ2_s_I1DjzIUgaod1VUvPiRO0jIIY0HtCs,3244
|
|
10
|
-
emdash_cli/commands/index.py,sha256=uFNC5whhU9JdF_59FeM99OPdzKLBTJLkLO6vp9pt944,6959
|
|
11
|
-
emdash_cli/commands/plan.py,sha256=BRiyIhfy_zz2PYy4Qo3a0t77GwHhdssZk6NImOkPi-w,2189
|
|
12
|
-
emdash_cli/commands/projectmd.py,sha256=4y4cn_yFw85jMUm52nGjpqnd-YWvs6ZNEMWJGeJC17Q,1605
|
|
13
|
-
emdash_cli/commands/research.py,sha256=xtI9_9emY7-rGQD5xJALTxtgTFmI4dplYW148dtTaTs,1553
|
|
14
|
-
emdash_cli/commands/rules.py,sha256=n85CCG0WNIBEsUK9STJetPmZxoypQtest5BGPsXl0ac,2712
|
|
15
|
-
emdash_cli/commands/search.py,sha256=DrSv_oN2xF1NaKCBICdyII7eupVRsDQ2ysW-TPSU0X0,1661
|
|
16
|
-
emdash_cli/commands/server.py,sha256=UTmLAVolT0krN9xCtMcCSvmQZ9k1QwpFFmXGg9BulRY,3459
|
|
17
|
-
emdash_cli/commands/skills.py,sha256=8N4279Hr8u2L8AgVjSTRVBLJBcXhN5DN7dn5fME62bs,9989
|
|
18
|
-
emdash_cli/commands/spec.py,sha256=qafDmzKyRH035p3xTm_VTUsQLDZblIzIg-dxjEPv6tM,1494
|
|
19
|
-
emdash_cli/commands/swarm.py,sha256=s_cntuorNdtNNTD2Qs1p2IcHghMrBMOQuturPS3y9mM,2661
|
|
20
|
-
emdash_cli/commands/tasks.py,sha256=TdyunjSV5w7jpNFwv0fTL-_No5Fyvdm7Z2nXqxWSJec,1635
|
|
21
|
-
emdash_cli/commands/team.py,sha256=K1-IJg6iG-9HMF_3JmpNDlNs1PYbb-ThFHU9KU_jKRo,1430
|
|
22
|
-
emdash_cli/keyboard.py,sha256=haYYAuhYGtdjomzhIFy_3Z3eN3BXfMdb4uRQjwB0tbk,4593
|
|
23
|
-
emdash_cli/main.py,sha256=c-faWp-jzf9a0BbXhVoPvPQfGWSryXpYfswehqZCYPM,2593
|
|
24
|
-
emdash_cli/server_manager.py,sha256=RrLteSHUmcFV4cyHJAEmgM9qHru2mJS08QNLWno6Y3Y,7051
|
|
25
|
-
emdash_cli/sse_renderer.py,sha256=aDOoHKglOkaYEXuKg937mH6yFPDxjU7Rqa_-APyM9Dc,23215
|
|
26
|
-
emdash_cli-0.1.30.dist-info/METADATA,sha256=czNXf-GzyfHKHe8g8Dfd7Bm7kI189H-VLRpFDVgtejc,738
|
|
27
|
-
emdash_cli-0.1.30.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
28
|
-
emdash_cli-0.1.30.dist-info/entry_points.txt,sha256=31CuYD0k-tM8csFWDunc-JoZTxXaifj3oIXz4V0p6F0,122
|
|
29
|
-
emdash_cli-0.1.30.dist-info/RECORD,,
|
|
File without changes
|