A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

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

Fix /context to show all sections and use a 40x5 grid

The /context bar was only showing a handful of hardcoded sections
(Character, Log, Session, Location, entities) and skipping Style,
Transcript, PlayerKnowledge, and Initiative entirely. Also the single-row
bar crammed everything into 30 chars where every section got min-width 1,
making the proportions meaningless.

Now it loops over all context_parts, so nothing gets silently dropped.
The bar is a 40x5 grid where each cell is 1k tokens — 200 cells for the
200k window. Token estimates are prefixed with ~ since they're len//4
heuristics, and the header shows real API token counts from the last turn
when available.

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

+68 -35
+68 -35
src/storied/cli.py
··· 381 381 stats = engine.get_context_stats() 382 382 console.print() 383 383 384 - # Header with model, time, and context estimate 385 - context_k = stats["context_total"] / 1000 384 + # Header: real API usage if available, estimate otherwise 386 385 limit_k = stats["model_limit"] / 1000 387 386 game_time = engine.get_current_time() 387 + if stats["last_input"] > 0: 388 + input_k = stats["last_input"] / 1000 389 + usage_str = f"{input_k:.1f}k/{limit_k:.0f}k tokens (last turn)" 390 + else: 391 + context_k = stats["context_total"] / 1000 392 + usage_str = f"~{context_k:.1f}k/{limit_k:.0f}k system tokens (estimated)" 388 393 console.print( 389 - f"[bold]Context Usage[/bold] [dim]({game_time})[/dim] " 390 - f"[dim]{engine.model} · ~{context_k:.1f}k/{limit_k:.0f}k system tokens[/dim]" 394 + f"[bold]Context Usage[/bold] [dim]({game_time})[/dim]" 395 + f" [dim]{engine.model} · {usage_str}[/dim]" 391 396 ) 392 397 393 - # Build visual bar (30 chars wide) 394 - bar_width = 30 395 - limit = stats["model_limit"] 396 - bar_parts = [ 398 + # Color mapping for context sections 399 + _SECTION_COLORS: dict[str, str] = { 400 + "Style": "dim", 401 + "Character": "green", 402 + "Log": "bright_cyan", 403 + "Transcript": "blue", 404 + "Session": "yellow", 405 + "PlayerKnowledge": "bright_green", 406 + "Location": "cyan", 407 + "Initiative": "red", 408 + } 409 + 410 + # Build section list from all context parts 411 + sections: list[tuple[str, int, str]] = [ 397 412 ("DM Instructions", stats["system_prompt"], "bright_blue"), 398 - ("Character", stats["context_parts"].get("Character", 0), "green"), 399 - ("Log", stats["context_parts"].get("Log", 0), "bright_cyan"), 400 - ("Session", stats["context_parts"].get("Session", 0), "yellow"), 401 413 ] 402 - if "Location" in stats["context_parts"]: 403 - bar_parts.append(("Location", stats["context_parts"]["Location"], "cyan")) 404 - for key, val in stats["context_parts"].items(): 414 + for key, tokens in stats["context_parts"].items(): 405 415 if key.startswith("Entity:") or key.startswith("Linked:"): 406 - bar_parts.append((key.split(":", 1)[1], val, "magenta")) 416 + label = key.split(":", 1)[1] 417 + color = "magenta" if key.startswith("Entity:") else "dark_magenta" 418 + else: 419 + label = key 420 + color = _SECTION_COLORS.get(key, "white") 421 + sections.append((label, tokens, color)) 407 422 408 - bar = "" 409 - legend_items = [] 410 - for name, tokens, color in bar_parts: 411 - if tokens > 0: 412 - width = max(1, int((tokens / limit) * bar_width)) 413 - bar += f"[{color}]{'█' * width}[/{color}]" 414 - pct_part = (tokens / limit) * 100 415 - legend_items.append((name, tokens, pct_part, color)) 423 + # 40x5 grid (200 cells = 200k tokens, 1 cell = 1k) 424 + grid_w, grid_h = 40, 5 425 + total_cells = grid_w * grid_h 426 + cells: list[str] = [] 427 + legend_items: list[tuple[str, int, str]] = [] 428 + for name, tokens, color in sections: 429 + n_cells = round(tokens / 1_000) 430 + if tokens > 0 and n_cells == 0: 431 + n_cells = 1 432 + if n_cells > 0: 433 + cells.extend([color] * n_cells) 434 + legend_items.append((name, tokens, color)) 416 435 417 - used_width = sum(max(1, int((t / limit) * bar_width)) for _, t, _ in bar_parts if t > 0) 418 - remaining_width = bar_width - used_width 419 - if remaining_width > 0: 420 - bar += f"[dim]{'░' * remaining_width}[/dim]" 436 + # Pad remaining cells 437 + remaining = total_cells - len(cells) 438 + if remaining > 0: 439 + cells.extend(["dim"] * remaining) 421 440 422 - console.print(bar) 441 + # Render grid 442 + for row in range(grid_h): 443 + line = "" 444 + for col in range(grid_w): 445 + idx = row * grid_w + col 446 + c = cells[idx] if idx < len(cells) else "dim" 447 + char = "█" if c != "dim" else "░" 448 + line += f"[{c}]{char}[/{c}]" 449 + console.print(line) 423 450 424 - for name, tokens, pct_part, color in legend_items: 425 - tokens_str = f"{tokens:,}" if tokens < 1000 else f"{tokens/1000:.1f}k" 426 - console.print(f" [{color}]█[/{color}] {name}: [dim]{tokens_str} ({pct_part:.1f}%)[/dim]") 451 + # Legend 452 + for name, tokens, color in legend_items: 453 + tokens_str = f"~{tokens:,}" if tokens < 1_000 else f"~{tokens / 1_000:.1f}k" 454 + console.print(f" [{color}]█[/{color}] {name}: [dim]{tokens_str}[/dim]") 427 455 428 - # Session totals 456 + # Session totals (actual API counts) 429 457 if stats["total_input"] > 0: 430 458 console.print() 431 - console.print("[dim]Session totals:[/dim]") 459 + last_in = f"{stats['last_input']:,}" 460 + last_out = f"{stats['last_output']:,}" 461 + total_in = f"{stats['total_input']:,}" 462 + total_out = f"{stats['total_output']:,}" 432 463 console.print( 433 - f" [dim]Tokens: {stats['total_input']:,} in · " 434 - f"{stats['total_output']:,} out[/dim]" 464 + f" [dim]Last turn: {last_in} in · {last_out} out[/dim]" 465 + ) 466 + console.print( 467 + f" [dim]Session: {total_in} in · {total_out} out[/dim]" 435 468 ) 436 469 437 470 console.print()