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.
Files changed (32) hide show
  1. emdash_cli/__init__.py +15 -0
  2. emdash_cli/client.py +156 -0
  3. emdash_cli/clipboard.py +30 -61
  4. emdash_cli/commands/agent/__init__.py +14 -0
  5. emdash_cli/commands/agent/cli.py +100 -0
  6. emdash_cli/commands/agent/constants.py +53 -0
  7. emdash_cli/commands/agent/file_utils.py +178 -0
  8. emdash_cli/commands/agent/handlers/__init__.py +41 -0
  9. emdash_cli/commands/agent/handlers/agents.py +421 -0
  10. emdash_cli/commands/agent/handlers/auth.py +69 -0
  11. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  12. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  13. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  14. emdash_cli/commands/agent/handlers/misc.py +200 -0
  15. emdash_cli/commands/agent/handlers/rules.py +394 -0
  16. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  17. emdash_cli/commands/agent/handlers/setup.py +582 -0
  18. emdash_cli/commands/agent/handlers/skills.py +440 -0
  19. emdash_cli/commands/agent/handlers/todos.py +98 -0
  20. emdash_cli/commands/agent/handlers/verify.py +648 -0
  21. emdash_cli/commands/agent/interactive.py +657 -0
  22. emdash_cli/commands/agent/menus.py +728 -0
  23. emdash_cli/commands/agent.py +7 -856
  24. emdash_cli/commands/server.py +99 -40
  25. emdash_cli/server_manager.py +70 -10
  26. emdash_cli/session_store.py +321 -0
  27. emdash_cli/sse_renderer.py +256 -110
  28. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
  29. emdash_cli-0.1.46.dist-info/RECORD +49 -0
  30. emdash_cli-0.1.30.dist-info/RECORD +0 -29
  31. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
  32. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
@@ -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
- self._clear_waiting()
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
- self._current_tool = {"name": name, "args": args, "start_time": time.time()}
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
- # Format args summary (compact)
268
- args_summary = self._format_args_summary(args)
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) > 40:
288
- summary = "..." + path[-37:]
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"])[:30]
327
+ summary = str(args["pattern"])[:50]
293
328
 
294
- # Clear line and show progress
295
- line = f" [dim][/dim] [yellow]{spinner}[/yellow] [dim cyan]({agent_type})[/dim cyan] {self._subagent_tool_count} tools... [bold]{tool_name}[/bold] [dim]{summary}[/dim]"
296
- # Pad to clear previous content
297
- sys.stdout.write(f"\r{' ' * 120}\r")
298
- self.console.print(line, end="")
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
- """Render sub-agent spawn start with special UI."""
336
+ """Track sub-agent spawn state (rendering done by subagent_start event)."""
302
337
  agent_type = args.get("subagent_type", "Explore")
303
- description = args.get("description", "")
304
- prompt = args.get("prompt", "")
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
- # Truncate prompt for display
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
- if self._current_tool and self._current_tool.get("start_time"):
351
- elapsed = time.time() - self._current_tool["start_time"]
352
- if elapsed >= 0.1:
353
- duration = f" [dim]{elapsed:.1f}s[/dim]"
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
- args_summary = ""
356
- if self._current_tool:
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
- status_icon = "[green]✓[/green]"
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
- status_icon = "[red]✗[/red]"
364
- result_text = f"[red]{summary}[/red]" if summary else "[red]failed[/red]"
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
- # Clear the progress line
384
- sys.stdout.write(f"\r{' ' * 120}\r")
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 k, v in list(args.items())[:2]:
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 summary with collapsible indicator
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
- self.console.print(f" [dim]┃[/dim] [dim] {preview}[/dim]")
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
- self._pending_clarification = {
511
- "question": question,
512
- "context": context,
513
- "options": options,
514
- }
515
- else:
516
- self._pending_clarification = None
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.table import Table
522
- from rich.text import Text
686
+ from rich.markdown import Markdown
523
687
 
524
- title = data.get("title", "Plan")
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
- # Build plan display
693
+ # Render plan as markdown in a panel
535
694
  self.console.print()
536
695
  self.console.print(Panel(
537
- f"[bold]{title}[/bold]\n\n{summary}",
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
- """Render context frame update (post-agentic loop summary)."""
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.30
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.30)
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,,