emdash-cli 0.1.25__py3-none-any.whl → 0.1.35__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 +129 -0
- emdash_cli/clipboard.py +123 -0
- emdash_cli/commands/agent.py +526 -34
- emdash_cli/session_store.py +321 -0
- emdash_cli/sse_renderer.py +224 -119
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.dist-info}/METADATA +4 -2
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.dist-info}/RECORD +10 -8
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.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
|
|
@@ -83,6 +86,9 @@ class SSERenderer:
|
|
|
83
86
|
final_response = ""
|
|
84
87
|
interrupted = False
|
|
85
88
|
self._last_thinking = None # Reset thinking storage
|
|
89
|
+
self._pending_clarification = None # Reset clarification state
|
|
90
|
+
self._plan_submitted = None # Reset plan state
|
|
91
|
+
self._plan_mode_requested = None # Reset plan mode request state
|
|
86
92
|
|
|
87
93
|
# Start spinner while waiting for first event
|
|
88
94
|
if self.verbose:
|
|
@@ -127,6 +133,7 @@ class SSERenderer:
|
|
|
127
133
|
"spec": self._spec,
|
|
128
134
|
"spec_submitted": self._spec_submitted,
|
|
129
135
|
"plan_submitted": self._plan_submitted,
|
|
136
|
+
"plan_mode_requested": self._plan_mode_requested,
|
|
130
137
|
"clarification": self._pending_clarification,
|
|
131
138
|
"interrupted": interrupted,
|
|
132
139
|
"thinking": self._last_thinking,
|
|
@@ -183,8 +190,10 @@ class SSERenderer:
|
|
|
183
190
|
if not isinstance(data, dict):
|
|
184
191
|
data = {}
|
|
185
192
|
|
|
186
|
-
# Clear waiting indicator when new event arrives
|
|
187
|
-
|
|
193
|
+
# Clear waiting indicator when new event arrives (but not for sub-agent events)
|
|
194
|
+
subagent_id = data.get("subagent_id") if isinstance(data, dict) else None
|
|
195
|
+
if not subagent_id and not self._in_subagent_mode:
|
|
196
|
+
self._clear_waiting()
|
|
188
197
|
|
|
189
198
|
if event_type == "session_start":
|
|
190
199
|
self._render_session_start(data)
|
|
@@ -196,8 +205,14 @@ class SSERenderer:
|
|
|
196
205
|
self._waiting_for_next = True
|
|
197
206
|
if self.verbose:
|
|
198
207
|
self._start_spinner("thinking")
|
|
208
|
+
elif event_type == "subagent_start":
|
|
209
|
+
self._render_subagent_start(data)
|
|
210
|
+
elif event_type == "subagent_end":
|
|
211
|
+
self._render_subagent_end(data)
|
|
199
212
|
elif event_type == "thinking":
|
|
200
213
|
self._render_thinking(data)
|
|
214
|
+
elif event_type == "assistant_text":
|
|
215
|
+
self._render_assistant_text(data)
|
|
201
216
|
elif event_type == "progress":
|
|
202
217
|
self._render_progress(data)
|
|
203
218
|
elif event_type == "partial_response":
|
|
@@ -206,6 +221,8 @@ class SSERenderer:
|
|
|
206
221
|
return self._render_response(data)
|
|
207
222
|
elif event_type == "clarification":
|
|
208
223
|
self._render_clarification(data)
|
|
224
|
+
elif event_type == "plan_mode_requested":
|
|
225
|
+
self._render_plan_mode_requested(data)
|
|
209
226
|
elif event_type == "plan_submitted":
|
|
210
227
|
self._render_plan_submitted(data)
|
|
211
228
|
elif event_type == "error":
|
|
@@ -239,6 +256,9 @@ class SSERenderer:
|
|
|
239
256
|
self._tool_count = 0
|
|
240
257
|
self._completed_tools = []
|
|
241
258
|
|
|
259
|
+
# Start spinner while waiting for first tool
|
|
260
|
+
self._start_spinner("thinking")
|
|
261
|
+
|
|
242
262
|
def _render_tool_start(self, data: dict) -> None:
|
|
243
263
|
"""Render tool start event."""
|
|
244
264
|
if not self.verbose:
|
|
@@ -246,11 +266,22 @@ class SSERenderer:
|
|
|
246
266
|
|
|
247
267
|
name = data.get("name", "unknown")
|
|
248
268
|
args = data.get("args", {})
|
|
269
|
+
tool_id = data.get("tool_id")
|
|
249
270
|
subagent_id = data.get("subagent_id")
|
|
250
271
|
subagent_type = data.get("subagent_type")
|
|
251
272
|
|
|
252
273
|
self._tool_count += 1
|
|
253
|
-
|
|
274
|
+
|
|
275
|
+
# Store tool info for result rendering (keyed by tool_id for parallel support)
|
|
276
|
+
if not hasattr(self, '_pending_tools'):
|
|
277
|
+
self._pending_tools = {}
|
|
278
|
+
# Use tool_id if available, otherwise fall back to name
|
|
279
|
+
key = tool_id or name
|
|
280
|
+
self._pending_tools[key] = {"name": name, "args": args, "start_time": time.time(), "tool_id": tool_id}
|
|
281
|
+
self._current_tool = self._pending_tools[key]
|
|
282
|
+
|
|
283
|
+
# Stop spinner when tool starts
|
|
284
|
+
self._stop_spinner()
|
|
254
285
|
|
|
255
286
|
# Special handling for task tool (spawning sub-agents)
|
|
256
287
|
if name == "task":
|
|
@@ -264,59 +295,49 @@ class SSERenderer:
|
|
|
264
295
|
self._render_subagent_progress(subagent_type or "Agent", name, args)
|
|
265
296
|
return
|
|
266
297
|
|
|
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
|
-
)
|
|
298
|
+
# Don't print anything here - wait for tool_result to print complete line
|
|
299
|
+
# This handles parallel tool execution correctly
|
|
276
300
|
|
|
277
301
|
def _render_subagent_progress(self, agent_type: str, tool_name: str, args: dict) -> None:
|
|
278
302
|
"""Render sub-agent progress on a single updating line."""
|
|
279
303
|
self._spinner_idx = (self._spinner_idx + 1) % len(SPINNER_FRAMES)
|
|
280
304
|
spinner = SPINNER_FRAMES[self._spinner_idx]
|
|
281
305
|
|
|
306
|
+
# Use stored type if not provided
|
|
307
|
+
agent_type = agent_type or self._subagent_type or "Agent"
|
|
308
|
+
|
|
282
309
|
# Get a short summary of what's being done
|
|
283
310
|
summary = ""
|
|
284
311
|
if "path" in args:
|
|
285
312
|
path = str(args["path"])
|
|
286
313
|
# Shorten long paths
|
|
287
|
-
if len(path) >
|
|
288
|
-
summary = "..." + path[-
|
|
314
|
+
if len(path) > 30:
|
|
315
|
+
summary = "..." + path[-27:]
|
|
289
316
|
else:
|
|
290
317
|
summary = path
|
|
291
318
|
elif "pattern" in args:
|
|
292
|
-
summary = str(args["pattern"])[:
|
|
319
|
+
summary = str(args["pattern"])[:25]
|
|
293
320
|
|
|
294
|
-
#
|
|
295
|
-
line = f"
|
|
296
|
-
#
|
|
297
|
-
sys.stdout.write(f"\r{
|
|
298
|
-
|
|
321
|
+
# Build compact progress line
|
|
322
|
+
line = f" │ {spinner} ({agent_type}) {self._subagent_tool_count} tools... {tool_name} {summary}"
|
|
323
|
+
# Use ANSI: move to column 0, clear line, print
|
|
324
|
+
sys.stdout.write(f"\r\033[K{line}")
|
|
325
|
+
sys.stdout.flush()
|
|
299
326
|
|
|
300
327
|
def _render_agent_spawn_start(self, args: dict) -> None:
|
|
301
|
-
"""
|
|
328
|
+
"""Track sub-agent spawn state (rendering done by subagent_start event)."""
|
|
302
329
|
agent_type = args.get("subagent_type", "Explore")
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
|
|
331
|
+
# Enter sub-agent mode: stop spinner, track state
|
|
332
|
+
self._stop_spinner()
|
|
333
|
+
self._in_subagent_mode = True
|
|
305
334
|
|
|
306
335
|
# Reset sub-agent tracking
|
|
307
336
|
self._subagent_tool_count = 0
|
|
308
337
|
self._subagent_current_tool = None
|
|
338
|
+
self._subagent_type = agent_type
|
|
309
339
|
|
|
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}")
|
|
340
|
+
# Don't render here - subagent_start event will render the UI
|
|
320
341
|
|
|
321
342
|
def _render_tool_result(self, data: dict) -> None:
|
|
322
343
|
"""Render tool result event."""
|
|
@@ -342,31 +363,35 @@ class SSERenderer:
|
|
|
342
363
|
|
|
343
364
|
# Sub-agent events: don't print result lines, just keep updating progress
|
|
344
365
|
if subagent_id:
|
|
345
|
-
# Progress is already shown by _render_tool_start, nothing to do here
|
|
346
366
|
return
|
|
347
367
|
|
|
368
|
+
# Get tool info from pending tools (use tool_id if available)
|
|
369
|
+
pending_tools = getattr(self, '_pending_tools', {})
|
|
370
|
+
tool_id = data.get("tool_id")
|
|
371
|
+
key = tool_id or name
|
|
372
|
+
tool_info = pending_tools.pop(key, None) or self._current_tool or {}
|
|
373
|
+
args = tool_info.get("args", {})
|
|
374
|
+
|
|
348
375
|
# Calculate duration
|
|
349
376
|
duration = ""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
377
|
+
start_time = tool_info.get("start_time")
|
|
378
|
+
if start_time:
|
|
379
|
+
elapsed = time.time() - start_time
|
|
380
|
+
if elapsed >= 0.5: # Only show if >= 0.5s
|
|
381
|
+
duration = f" {elapsed:.1f}s"
|
|
354
382
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
args_summary = self._format_args_summary(self._current_tool.get("args", {}))
|
|
383
|
+
# Format args for display
|
|
384
|
+
args_display = self._format_tool_args(name, args)
|
|
358
385
|
|
|
386
|
+
# Build complete line: • ToolName(args)
|
|
359
387
|
if success:
|
|
360
|
-
|
|
361
|
-
result_text = f"[dim]{summary}[/dim]" if summary else ""
|
|
388
|
+
# Format: • tool(args) result 1.2s
|
|
389
|
+
result_text = f" [dim]{summary}[/dim]" if summary else ""
|
|
390
|
+
duration_text = f" [dim]{duration}[/dim]" if duration else ""
|
|
391
|
+
self.console.print(f" [green]✓[/green] [bold]{name}[/bold]({args_display}){result_text}{duration_text}")
|
|
362
392
|
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
|
-
)
|
|
393
|
+
error_text = summary or "failed"
|
|
394
|
+
self.console.print(f" [red]✗[/red] [bold]{name}[/bold]({args_display}) [red]{error_text}[/red]")
|
|
370
395
|
|
|
371
396
|
self._completed_tools.append({
|
|
372
397
|
"name": name,
|
|
@@ -380,8 +405,11 @@ class SSERenderer:
|
|
|
380
405
|
success = data.get("success", True)
|
|
381
406
|
result_data = data.get("data") or {}
|
|
382
407
|
|
|
383
|
-
#
|
|
384
|
-
|
|
408
|
+
# Exit sub-agent mode
|
|
409
|
+
self._in_subagent_mode = False
|
|
410
|
+
|
|
411
|
+
# Clear the progress line and move to new line
|
|
412
|
+
sys.stdout.write(f"\r\033[K")
|
|
385
413
|
sys.stdout.flush()
|
|
386
414
|
|
|
387
415
|
# Calculate duration
|
|
@@ -415,6 +443,66 @@ class SSERenderer:
|
|
|
415
443
|
self.console.print()
|
|
416
444
|
self._current_tool = None
|
|
417
445
|
self._subagent_tool_count = 0
|
|
446
|
+
self._subagent_type = None
|
|
447
|
+
|
|
448
|
+
def _render_subagent_start(self, data: dict) -> None:
|
|
449
|
+
"""Render subagent start event - shows when Explore/Plan agent is spawned."""
|
|
450
|
+
agent_type = data.get("agent_type", "Agent")
|
|
451
|
+
prompt = data.get("prompt", "")
|
|
452
|
+
description = data.get("description", "")
|
|
453
|
+
|
|
454
|
+
# Stop any existing spinner
|
|
455
|
+
self._stop_spinner()
|
|
456
|
+
|
|
457
|
+
# Truncate prompt for display
|
|
458
|
+
prompt_display = prompt[:80] + "..." if len(prompt) > 80 else prompt
|
|
459
|
+
|
|
460
|
+
self.console.print()
|
|
461
|
+
# Use different colors for different agent types
|
|
462
|
+
if agent_type == "Plan":
|
|
463
|
+
self.console.print(f" [bold blue]◆ Spawning {agent_type} Agent[/bold blue]")
|
|
464
|
+
else:
|
|
465
|
+
self.console.print(f" [bold magenta]◆ Spawning {agent_type} Agent[/bold magenta]")
|
|
466
|
+
|
|
467
|
+
if description:
|
|
468
|
+
self.console.print(f" [dim]{description}[/dim]")
|
|
469
|
+
self.console.print(f" [cyan]→[/cyan] {prompt_display}")
|
|
470
|
+
|
|
471
|
+
# Enter subagent mode for tool tracking
|
|
472
|
+
self._in_subagent_mode = True
|
|
473
|
+
self._subagent_type = agent_type
|
|
474
|
+
self._subagent_tool_count = 0
|
|
475
|
+
self._subagent_start_time = time.time()
|
|
476
|
+
|
|
477
|
+
def _render_subagent_end(self, data: dict) -> None:
|
|
478
|
+
"""Render subagent end event - shows completion summary."""
|
|
479
|
+
agent_type = data.get("agent_type", "Agent")
|
|
480
|
+
success = data.get("success", True)
|
|
481
|
+
iterations = data.get("iterations", 0)
|
|
482
|
+
files_explored = data.get("files_explored", 0)
|
|
483
|
+
execution_time = data.get("execution_time", 0)
|
|
484
|
+
|
|
485
|
+
# Exit subagent mode
|
|
486
|
+
self._in_subagent_mode = False
|
|
487
|
+
|
|
488
|
+
if success:
|
|
489
|
+
self.console.print(
|
|
490
|
+
f" [green]✓[/green] {agent_type} completed [dim]({execution_time:.1f}s)[/dim]"
|
|
491
|
+
)
|
|
492
|
+
# Show stats
|
|
493
|
+
stats = []
|
|
494
|
+
if iterations > 0:
|
|
495
|
+
stats.append(f"{iterations} turns")
|
|
496
|
+
if files_explored > 0:
|
|
497
|
+
stats.append(f"{files_explored} files")
|
|
498
|
+
if stats:
|
|
499
|
+
self.console.print(f" [dim]{' · '.join(stats)}[/dim]")
|
|
500
|
+
else:
|
|
501
|
+
self.console.print(f" [red]✗[/red] {agent_type} failed")
|
|
502
|
+
|
|
503
|
+
self.console.print()
|
|
504
|
+
self._subagent_type = None
|
|
505
|
+
self._subagent_tool_count = 0
|
|
418
506
|
|
|
419
507
|
def _format_args_summary(self, args: dict) -> str:
|
|
420
508
|
"""Format args into a compact summary string."""
|
|
@@ -422,7 +510,7 @@ class SSERenderer:
|
|
|
422
510
|
return ""
|
|
423
511
|
|
|
424
512
|
parts = []
|
|
425
|
-
for
|
|
513
|
+
for _, v in list(args.items())[:2]:
|
|
426
514
|
v_str = str(v)
|
|
427
515
|
if len(v_str) > 40:
|
|
428
516
|
v_str = v_str[:37] + "..."
|
|
@@ -430,6 +518,41 @@ class SSERenderer:
|
|
|
430
518
|
|
|
431
519
|
return " ".join(parts)
|
|
432
520
|
|
|
521
|
+
def _format_tool_args(self, tool_name: str, args: dict) -> str:
|
|
522
|
+
"""Format tool args in Claude Code style: ToolName(key_arg_value).
|
|
523
|
+
|
|
524
|
+
Shows the most relevant arg for each tool type.
|
|
525
|
+
"""
|
|
526
|
+
if not args:
|
|
527
|
+
return ""
|
|
528
|
+
|
|
529
|
+
# Tool-specific formatting for cleaner display
|
|
530
|
+
if tool_name in ("glob", "grep", "semantic_search"):
|
|
531
|
+
pattern = args.get("pattern", args.get("query", ""))
|
|
532
|
+
if pattern:
|
|
533
|
+
return f'[dim]pattern:[/dim] "{pattern}"' if len(pattern) < 50 else f'[dim]pattern:[/dim] "{pattern[:47]}..."'
|
|
534
|
+
elif tool_name in ("read_file", "write_to_file", "list_files"):
|
|
535
|
+
path = args.get("path", "")
|
|
536
|
+
if path:
|
|
537
|
+
return f"[dim]{path}[/dim]"
|
|
538
|
+
elif tool_name == "bash":
|
|
539
|
+
cmd = args.get("command", "")
|
|
540
|
+
if cmd:
|
|
541
|
+
return f"[dim]{cmd[:60]}{'...' if len(cmd) > 60 else ''}[/dim]"
|
|
542
|
+
elif tool_name == "edit_file":
|
|
543
|
+
path = args.get("path", "")
|
|
544
|
+
if path:
|
|
545
|
+
return f"[dim]{path}[/dim]"
|
|
546
|
+
|
|
547
|
+
# Default: show first arg value
|
|
548
|
+
if args:
|
|
549
|
+
first_val = str(list(args.values())[0])
|
|
550
|
+
if len(first_val) > 50:
|
|
551
|
+
first_val = first_val[:47] + "..."
|
|
552
|
+
return f"[dim]{first_val}[/dim]"
|
|
553
|
+
|
|
554
|
+
return ""
|
|
555
|
+
|
|
433
556
|
def _render_thinking(self, data: dict) -> None:
|
|
434
557
|
"""Render thinking event.
|
|
435
558
|
|
|
@@ -442,15 +565,15 @@ class SSERenderer:
|
|
|
442
565
|
|
|
443
566
|
# Check if this is extended thinking (long content) vs short progress message
|
|
444
567
|
if len(message) > 200:
|
|
445
|
-
# Extended thinking - show
|
|
568
|
+
# Extended thinking - show full content
|
|
446
569
|
self._stop_spinner()
|
|
447
570
|
lines = message.strip().split("\n")
|
|
448
|
-
preview = lines[0][:80] + "..." if len(lines[0]) > 80 else lines[0]
|
|
449
571
|
line_count = len(lines)
|
|
450
572
|
char_count = len(message)
|
|
451
573
|
|
|
452
574
|
self.console.print(f" [dim]┃[/dim] [dim italic]💭 Thinking ({char_count:,} chars, {line_count} lines)[/dim italic]")
|
|
453
|
-
|
|
575
|
+
for line in lines:
|
|
576
|
+
self.console.print(f" [dim]┃[/dim] [dim] {line}[/dim]")
|
|
454
577
|
|
|
455
578
|
# Store thinking for potential later display
|
|
456
579
|
self._last_thinking = message
|
|
@@ -458,6 +581,24 @@ class SSERenderer:
|
|
|
458
581
|
# Short progress message
|
|
459
582
|
self.console.print(f" [dim]┃[/dim] [dim italic]💭 {message}[/dim italic]")
|
|
460
583
|
|
|
584
|
+
def _render_assistant_text(self, data: dict) -> None:
|
|
585
|
+
"""Render intermediate assistant text (between tool calls)."""
|
|
586
|
+
if not self.verbose:
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
content = data.get("content", "").strip()
|
|
590
|
+
if not content:
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
# Stop spinner while showing text
|
|
594
|
+
self._stop_spinner()
|
|
595
|
+
|
|
596
|
+
# Show as bullet point like Claude Code (cyan for assistant reasoning)
|
|
597
|
+
# Truncate long content
|
|
598
|
+
if len(content) > 200:
|
|
599
|
+
content = content[:197] + "..."
|
|
600
|
+
self.console.print(f" [cyan]•[/cyan] [italic]{content}[/italic]")
|
|
601
|
+
|
|
461
602
|
def _render_progress(self, data: dict) -> None:
|
|
462
603
|
"""Render progress event."""
|
|
463
604
|
if not self.verbose:
|
|
@@ -507,73 +648,47 @@ class SSERenderer:
|
|
|
507
648
|
self.console.print(f" [yellow][{i}][/yellow] {opt}")
|
|
508
649
|
self.console.print()
|
|
509
650
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
651
|
+
# Always store clarification (with or without options)
|
|
652
|
+
self._pending_clarification = {
|
|
653
|
+
"question": question,
|
|
654
|
+
"context": context,
|
|
655
|
+
"options": options,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
def _render_plan_mode_requested(self, data: dict) -> None:
|
|
659
|
+
"""Render plan mode request event and store for menu display."""
|
|
660
|
+
from rich.panel import Panel
|
|
661
|
+
|
|
662
|
+
reason = data.get("reason", "")
|
|
663
|
+
|
|
664
|
+
# Store the request data for the CLI to show the menu
|
|
665
|
+
self._plan_mode_requested = data
|
|
666
|
+
|
|
667
|
+
# Display the request
|
|
668
|
+
self.console.print()
|
|
669
|
+
self.console.print(Panel(
|
|
670
|
+
f"[bold]Request to Enter Plan Mode[/bold]\n\n{reason}",
|
|
671
|
+
title="[yellow]⚡ Plan Mode Request[/yellow]",
|
|
672
|
+
border_style="yellow",
|
|
673
|
+
))
|
|
517
674
|
|
|
518
675
|
def _render_plan_submitted(self, data: dict) -> None:
|
|
519
676
|
"""Render plan submission event and store for menu display."""
|
|
520
677
|
from rich.panel import Panel
|
|
521
|
-
from rich.
|
|
522
|
-
from rich.text import Text
|
|
678
|
+
from rich.markdown import Markdown
|
|
523
679
|
|
|
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", "")
|
|
680
|
+
plan = data.get("plan", "")
|
|
530
681
|
|
|
531
682
|
# Store the plan data for the CLI to show the menu
|
|
532
683
|
self._plan_submitted = data
|
|
533
684
|
|
|
534
|
-
#
|
|
685
|
+
# Render plan as markdown in a panel
|
|
535
686
|
self.console.print()
|
|
536
687
|
self.console.print(Panel(
|
|
537
|
-
|
|
688
|
+
Markdown(plan),
|
|
538
689
|
title="[cyan]📋 Plan[/cyan]",
|
|
539
690
|
border_style="cyan",
|
|
540
691
|
))
|
|
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
692
|
self.console.print()
|
|
578
693
|
|
|
579
694
|
def _render_error(self, data: dict) -> None:
|
|
@@ -611,7 +726,6 @@ class SSERenderer:
|
|
|
611
726
|
entities_found = adding.get("entities_found", 0)
|
|
612
727
|
context_tokens = adding.get("context_tokens", 0)
|
|
613
728
|
context_breakdown = adding.get("context_breakdown", {})
|
|
614
|
-
largest_messages = adding.get("largest_messages", [])
|
|
615
729
|
|
|
616
730
|
# Get reading stats
|
|
617
731
|
item_count = reading.get("item_count", 0)
|
|
@@ -636,15 +750,6 @@ class SSERenderer:
|
|
|
636
750
|
if breakdown_parts:
|
|
637
751
|
self.console.print(f" [dim]Breakdown: {' | '.join(breakdown_parts)}[/dim]")
|
|
638
752
|
|
|
639
|
-
# Show largest messages (context hogs)
|
|
640
|
-
if largest_messages:
|
|
641
|
-
self.console.print(f" [yellow]Largest messages:[/yellow]")
|
|
642
|
-
for msg in largest_messages[:5]:
|
|
643
|
-
label = msg.get("label", "unknown")
|
|
644
|
-
tokens = msg.get("tokens", 0)
|
|
645
|
-
preview = msg.get("preview", "")[:50].replace("\n", " ")
|
|
646
|
-
self.console.print(f" [dim]{tokens:,} tokens[/dim] - {label}: {preview}...")
|
|
647
|
-
|
|
648
753
|
# Show other stats
|
|
649
754
|
stats = []
|
|
650
755
|
if step_count > 0:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emdash-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.35
|
|
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,8 +10,10 @@ 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
|
|
13
14
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
14
|
-
Requires-Dist: emdash-core (>=0.1.
|
|
15
|
+
Requires-Dist: emdash-core (>=0.1.35)
|
|
15
16
|
Requires-Dist: httpx (>=0.25.0)
|
|
17
|
+
Requires-Dist: pillow (>=10.0.0) ; extra == "images"
|
|
16
18
|
Requires-Dist: prompt_toolkit (>=3.0.43,<4.0.0)
|
|
17
19
|
Requires-Dist: rich (>=13.7.0)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
emdash_cli/__init__.py,sha256=
|
|
2
|
-
emdash_cli/client.py,sha256=
|
|
1
|
+
emdash_cli/__init__.py,sha256=21JmQcWge3QOM4hLjtBdu-7AoxJonJuIwMeobWIjr2w,690
|
|
2
|
+
emdash_cli/client.py,sha256=MZCyajCM7UghFVv_lKoPAaX0lUUDTe4FI4qlIPCYvcc,21008
|
|
3
|
+
emdash_cli/clipboard.py,sha256=hcg5sbIhbixqzpJdonoFLGBlSo2AKjplNrWy5PGnqaY,3564
|
|
3
4
|
emdash_cli/commands/__init__.py,sha256=D9edXBHm69tueUtE4DggTA1_Yjsl9YZaKjBVDY2D_gQ,712
|
|
4
|
-
emdash_cli/commands/agent.py,sha256=
|
|
5
|
+
emdash_cli/commands/agent.py,sha256=ZBCJWxX-Ze6UlmU3POO3EMjCdOc6D_akxlr6T4U-p74,48445
|
|
5
6
|
emdash_cli/commands/analyze.py,sha256=c9ztbv0Ra7g2AlDmMOy-9L51fDVuoqbuzxnRfomoFIQ,4403
|
|
6
7
|
emdash_cli/commands/auth.py,sha256=SpWdqO1bJCgt4x1B4Pr7hNOucwTuBFJ1oGPOzXtvwZM,3816
|
|
7
8
|
emdash_cli/commands/db.py,sha256=nZK7gLDVE2lAQVYrMx6Swscml5OAtkbg-EcSNSvRIlA,2922
|
|
@@ -21,8 +22,9 @@ emdash_cli/commands/team.py,sha256=K1-IJg6iG-9HMF_3JmpNDlNs1PYbb-ThFHU9KU_jKRo,1
|
|
|
21
22
|
emdash_cli/keyboard.py,sha256=haYYAuhYGtdjomzhIFy_3Z3eN3BXfMdb4uRQjwB0tbk,4593
|
|
22
23
|
emdash_cli/main.py,sha256=c-faWp-jzf9a0BbXhVoPvPQfGWSryXpYfswehqZCYPM,2593
|
|
23
24
|
emdash_cli/server_manager.py,sha256=RrLteSHUmcFV4cyHJAEmgM9qHru2mJS08QNLWno6Y3Y,7051
|
|
24
|
-
emdash_cli/
|
|
25
|
-
emdash_cli
|
|
26
|
-
emdash_cli-0.1.
|
|
27
|
-
emdash_cli-0.1.
|
|
28
|
-
emdash_cli-0.1.
|
|
25
|
+
emdash_cli/session_store.py,sha256=GjS73GLSZ3oTNtrFHMcyiP6GnH0Dvfvs6r4s3-bfEaM,9424
|
|
26
|
+
emdash_cli/sse_renderer.py,sha256=QRykGh1BirtRC_2_3X48zJ0gctDEF7OV5VgiM0QUwtQ,27750
|
|
27
|
+
emdash_cli-0.1.35.dist-info/METADATA,sha256=Imi3rtfGyvWLC_ddLV8GiPPeSQlavqcB78XgowoJufg,738
|
|
28
|
+
emdash_cli-0.1.35.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
29
|
+
emdash_cli-0.1.35.dist-info/entry_points.txt,sha256=31CuYD0k-tM8csFWDunc-JoZTxXaifj3oIXz4V0p6F0,122
|
|
30
|
+
emdash_cli-0.1.35.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|