A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Clean up spacing between narrative text and tool notifications

Tool notifications had leading/trailing \n baked in, and the CLI added
more on top — stacking up to 3-4 blank lines between a prompt and the
DM's first output. Now the engine returns bare `[Label...]` strings and
the CLI handles all spacing: one blank line between the rule and the
response, one blank line separating text from tool groups, and leading
newlines from Claude's stream are stripped so the gap is consistent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+14 -9
+11 -6
src/storied/cli.py
··· 488 488 489 489 try: 490 490 console.print(Rule(style="dim blue")) 491 - console.print() # Blank line before DM response 492 491 renderer = StreamRenderer(console) 493 492 prev_type: str | None = None 493 + got_text = False 494 494 495 495 for chunk in engine.stream_action(action): 496 - if chunk.startswith("\n[") or chunk.startswith("Rolled "): 497 - if prev_type != "tool": 496 + if chunk.startswith("[") and chunk.endswith("]") and "..." in chunk: 497 + if prev_type == "text": 498 498 renderer.flush() 499 - console.file.write("\n") 500 - console.print() 501 - console.print(f"[dim]{chunk.strip()}[/dim]") 499 + console.file.write("\n\n") 500 + console.print(f"[dim]{chunk}[/dim]") 502 501 prev_type = "tool" 503 502 else: 504 503 if prev_type == "tool": 505 504 console.print() 505 + if not got_text: 506 + chunk = chunk.lstrip("\n") 507 + if not chunk: 508 + continue 509 + chunk = "\n" + chunk 510 + got_text = True 506 511 renderer.feed(chunk) 507 512 prev_type = "text" 508 513
+3 -3
src/storied/engine.py
··· 73 73 "end_initiative": "Ending initiative", 74 74 } 75 75 label = labels.get(short, short) 76 - return f"\n[{label}...]\n" 76 + return f"[{label}...]" 77 77 78 78 79 79 class DMEngine: ··· 435 435 if short == "roll" and not self.debug: 436 436 deferred_notification = True 437 437 elif self.debug: 438 - yield f"\n[→ {short}(...)]\n" 438 + yield f"[→ {short}(...)]" 439 439 deferred_notification = False 440 440 else: 441 441 yield _tool_notification(name) ··· 448 448 if deferred_notification and current_tool_name == "roll": 449 449 reason = _extract_roll_reason(current_tool_json) 450 450 label = f"Rolling {reason}" if reason else "Rolling" 451 - yield f"\n[{label}...]\n" 451 + yield f"[{label}...]" 452 452 453 453 if self.debug and current_tool_json: 454 454 truncated = current_tool_json[:200]