fix journal_update bugs, persist last prompt across restarts
- Coerce string add/done to list in journal_update tool - Rewrite _update_journal with section-based parsing (no broken index tracking) - Add duplicate prevention, blank line collapsing - Save last DM prompt to session/last_prompt.md so game resumes from last scene on restart instead of regenerating
This commit is contained in:
parent
35c04bdbca
commit
326c8b7ba8
229
tools/engine.py
229
tools/engine.py
@ -53,10 +53,6 @@ class TurnResult:
|
|||||||
book_log: str = ""
|
book_log: str = ""
|
||||||
user_prompt: str = ""
|
user_prompt: str = ""
|
||||||
ambience: Optional[str] = None
|
ambience: Optional[str] = None
|
||||||
character_updates: Optional[str] = None
|
|
||||||
world_updates: Optional[str] = None
|
|
||||||
journal_add: list[str] = field(default_factory=list)
|
|
||||||
journal_done: list[str] = field(default_factory=list)
|
|
||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
debug_info: str = ""
|
debug_info: str = ""
|
||||||
|
|
||||||
@ -109,21 +105,26 @@ The **finalize_turn** tool produces all data for this turn:
|
|||||||
- **book_log** — Narrative of what happened this turn. Appended to the story book.
|
- **book_log** — Narrative of what happened this turn. Appended to the story book.
|
||||||
- **user_prompt** — What the player sees next: describe the situation and ask what they do.
|
- **user_prompt** — What the player sees next: describe the situation and ask what they do.
|
||||||
- **ambience** — One of: silence, calm, combat, dungeon, forest, tavern, tension, town, wilds.
|
- **ambience** — One of: silence, calm, combat, dungeon, forest, tavern, tension, town, wilds.
|
||||||
- **character_updates** — Full character sheet (ONLY if HP/cash/gear/stats changed, otherwise omit).
|
|
||||||
- **world_updates** — Full world state (ONLY if NPCs/locations/threads changed, otherwise omit).
|
|
||||||
- **journal_add** — New TODO items (quests, goals, leads).
|
|
||||||
- **journal_done** — Completed TODO items.
|
|
||||||
|
|
||||||
### Journal & Quest Tracking
|
### Journal & Quest Tracking
|
||||||
|
|
||||||
The journal is the player's quest log and TODO list rolled into one. Use it to track the bigger picture:
|
The journal is the player's quest log and TODO list. Use dedicated tools to manage it:
|
||||||
|
|
||||||
- **Add quests** as they arise: `journal_add: ["Investigate the Weeper beneath the mill"]`
|
- **`journal_get`** — Read the full journal to review quests.
|
||||||
- **Mark sub-tasks** as they emerge: `journal_add: ["Find a way to open the iron grate", "Question Rina about the cult"]`
|
- **`journal_update`** — Add new quests/goals via `"add"` and mark completed via `"done"`.
|
||||||
- **Mark completed** when resolved: `journal_done: ["Investigate the Weeper beneath the mill"]`
|
- **Add quests** as they arise: `{"add": ["Investigate the Weeper beneath the mill"]}`
|
||||||
|
- **Mark sub-tasks** as they emerge: `{"add": ["Find a way to open the iron grate", "Question Rina about the cult"]}`
|
||||||
|
- **Mark completed** when resolved: `{"done": ["Investigate the Weeper beneath the mill"]}`
|
||||||
- **Keep descriptions specific** — vague entries like "Explore the dungeon" are not helpful.
|
- **Keep descriptions specific** — vague entries like "Explore the dungeon" are not helpful.
|
||||||
- **Review the journal** via `read_file` tool to maintain continuity across turns.
|
- **Review the journal** regularly to maintain continuity.
|
||||||
- Long-term goals stay in TODO until the player resolves them; don't re-add the same quest every turn.
|
- Long-term goals stay in TODO until resolved; don't re-add the same quest every turn.
|
||||||
|
|
||||||
|
### Character & World State
|
||||||
|
|
||||||
|
To read or update state files, use the dedicated tools:
|
||||||
|
|
||||||
|
- **`character_get`** / **`character_update`** — Read or replace the full character sheet. ONLY update when HP/cash/gear/stats change.
|
||||||
|
- **`world_get`** / **`world_update`** — Read or replace the full world state. ONLY update when NPCs/locations/threads change.
|
||||||
|
|
||||||
IMPORTANT: You MUST call **finalize_turn** to end the turn. Until then you will be called again to continue thinking and gathering information.
|
IMPORTANT: You MUST call **finalize_turn** to end the turn. Until then you will be called again to continue thinking and gathering information.
|
||||||
|
|
||||||
@ -148,7 +149,13 @@ Every tool call **must** include a `"dm_status"` string in `args` — a short, p
|
|||||||
- **read_file** — Read a game state file. `{"file": "character|world|book|log|journal", "dm_status": "..."}`
|
- **read_file** — Read a game state file. `{"file": "character|world|book|log|journal", "dm_status": "..."}`
|
||||||
- **roll** — Auto-roll dice (outcome shown in status). `{"dice": "2d6", "modifier": "-1", "dm_status": "..."}`
|
- **roll** — Auto-roll dice (outcome shown in status). `{"dice": "2d6", "modifier": "-1", "dm_status": "..."}`
|
||||||
- **player_roll** — Ask the player to roll physical dice. **Use when the outcome is uncertain.** `{"dice": "2d6", "reason": "why", "dm_status": "..."}`
|
- **player_roll** — Ask the player to roll physical dice. **Use when the outcome is uncertain.** `{"dice": "2d6", "reason": "why", "dm_status": "..."}`
|
||||||
- **finalize_turn** — **Complete the turn.** Provide all turn data as args. **Must include** `"dm_status"`.
|
- **character_get** — Read the full character sheet. `{"dm_status": "..."}`
|
||||||
|
- **character_update** — Replace the character sheet (full content). `{"content": "...", "dm_status": "..."}`
|
||||||
|
- **world_get** — Read the full world state. `{"dm_status": "..."}`
|
||||||
|
- **world_update** — Replace the world state (full content). `{"content": "...", "dm_status": "..."}`
|
||||||
|
- **journal_get** — Read the journal (TODO / DONE). `{"dm_status": "..."}`
|
||||||
|
- **journal_update** — Add or complete journal entries. `{"add": [...], "done": [...], "dm_status": "..."}`
|
||||||
|
- **finalize_turn** — **Complete the turn.** Provide all turn data as args.
|
||||||
|
|
||||||
When the player makes a choice, resolve it with the dice mechanics above. Describe the action, roll dice implicitly (describe the outcome, don't say "rolling dice"), apply damage/effects, and update state.
|
When the player makes a choice, resolve it with the dice mechanics above. Describe the action, roll dice implicitly (describe the outcome, don't say "rolling dice"), apply damage/effects, and update state.
|
||||||
|
|
||||||
@ -437,16 +444,36 @@ class GameEngine:
|
|||||||
"description": "Ask the player to physically roll dice and enter the result.",
|
"description": "Ask the player to physically roll dice and enter the result.",
|
||||||
"args": {"dice": "e.g. 2d6+1", "reason": "Why the roll is needed (shown to player)"},
|
"args": {"dice": "e.g. 2d6+1", "reason": "Why the roll is needed (shown to player)"},
|
||||||
},
|
},
|
||||||
|
"character_get": {
|
||||||
|
"description": "Read the full character sheet.",
|
||||||
|
"args": {},
|
||||||
|
},
|
||||||
|
"character_update": {
|
||||||
|
"description": "Replace the character sheet with a new full version (ONLY if HP/cash/gear/stats changed).",
|
||||||
|
"args": {"content": "Full character sheet markdown"},
|
||||||
|
},
|
||||||
|
"world_get": {
|
||||||
|
"description": "Read the full world state.",
|
||||||
|
"args": {},
|
||||||
|
},
|
||||||
|
"world_update": {
|
||||||
|
"description": "Replace the world state with a new full version (ONLY if NPCs/locations/threads changed).",
|
||||||
|
"args": {"content": "Full world state markdown"},
|
||||||
|
},
|
||||||
|
"journal_get": {
|
||||||
|
"description": "Read the journal (TODO / DONE).",
|
||||||
|
"args": {},
|
||||||
|
},
|
||||||
|
"journal_update": {
|
||||||
|
"description": "Add or complete journal entries.",
|
||||||
|
"args": {"add": "Optional: list of new TODO items", "done": "Optional: list of completed items"},
|
||||||
|
},
|
||||||
"finalize_turn": {
|
"finalize_turn": {
|
||||||
"description": "Complete the turn with all required data.",
|
"description": "Complete the turn with all required data.",
|
||||||
"args": {
|
"args": {
|
||||||
"book_log": "Narrative of what happened (appended to story book)",
|
"book_log": "Narrative of what happened (appended to story book)",
|
||||||
"user_prompt": "What the player sees next — describe and ask what they do",
|
"user_prompt": "What the player sees next — describe and ask what they do",
|
||||||
"ambience": "Optional: soundscape name",
|
"ambience": "Optional: soundscape name",
|
||||||
"character_updates": "Optional: full character sheet if changed",
|
|
||||||
"world_updates": "Optional: full world state if changed",
|
|
||||||
"journal_add": "Optional: list of new TODO items",
|
|
||||||
"journal_done": "Optional: list of completed TODO items",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -490,6 +517,45 @@ class GameEngine:
|
|||||||
mod_str = f" {'+' if mod >= 0 else ''}{mod}" if mod != 0 else ""
|
mod_str = f" {'+' if mod >= 0 else ''}{mod}" if mod != 0 else ""
|
||||||
return f"Roll: {dice_str}{mod_str} → [{', '.join(str(r) for r in rolls)}] = {total}"
|
return f"Roll: {dice_str}{mod_str} → [{', '.join(str(r) for r in rolls)}] = {total}"
|
||||||
|
|
||||||
|
def _tool_character_get(self, args: dict) -> str:
|
||||||
|
return self._read_file(CHAR_PATH) or "*Character sheet is empty.*"
|
||||||
|
|
||||||
|
def _tool_character_update(self, args: dict) -> str:
|
||||||
|
content = (args or {}).get("content", "")
|
||||||
|
if not content:
|
||||||
|
return "**Error:** `content` is required."
|
||||||
|
if not self._validate_update_size("character", content, CHAR_PATH):
|
||||||
|
return "**Error:** Update rejected — content is too short (likely a partial paste)."
|
||||||
|
CHAR_PATH.write_text(content.strip() + "\n")
|
||||||
|
return "Character sheet updated."
|
||||||
|
|
||||||
|
def _tool_world_get(self, args: dict) -> str:
|
||||||
|
return self._read_file(WORLD_PATH) or "*World state is empty.*"
|
||||||
|
|
||||||
|
def _tool_world_update(self, args: dict) -> str:
|
||||||
|
content = (args or {}).get("content", "")
|
||||||
|
if not content:
|
||||||
|
return "**Error:** `content` is required."
|
||||||
|
if not self._validate_update_size("world", content, WORLD_PATH):
|
||||||
|
return "**Error:** Update rejected — content is too short (likely a partial paste)."
|
||||||
|
WORLD_PATH.write_text(content.strip() + "\n")
|
||||||
|
return "World state updated."
|
||||||
|
|
||||||
|
def _tool_journal_get(self, args: dict) -> str:
|
||||||
|
return self._read_file(JOURNAL_PATH) or "*Journal is empty.*"
|
||||||
|
|
||||||
|
def _tool_journal_update(self, args: dict) -> str:
|
||||||
|
add = (args or {}).get("add", [])
|
||||||
|
done = (args or {}).get("done", [])
|
||||||
|
if isinstance(add, str):
|
||||||
|
add = [add]
|
||||||
|
if isinstance(done, str):
|
||||||
|
done = [done]
|
||||||
|
if not add and not done:
|
||||||
|
return "**Error:** Provide at least one of `add` or `done`."
|
||||||
|
self._update_journal(add=add, done=done)
|
||||||
|
return "Journal updated."
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _describe_tool_action(tool_name: str, args: dict) -> str:
|
def _describe_tool_action(tool_name: str, args: dict) -> str:
|
||||||
"""Return a user-facing status message for a tool call.
|
"""Return a user-facing status message for a tool call.
|
||||||
@ -508,6 +574,13 @@ class GameEngine:
|
|||||||
if tool_name == "read_file":
|
if tool_name == "read_file":
|
||||||
file = (args or {}).get("file", "")
|
file = (args or {}).get("file", "")
|
||||||
desc = read_descriptions.get(file, f"reading {file}")
|
desc = read_descriptions.get(file, f"reading {file}")
|
||||||
|
elif tool_name in ("character_get", "world_get", "journal_get"):
|
||||||
|
file = tool_name.replace("_get", "")
|
||||||
|
desc = read_descriptions.get(file, f"reading {file}")
|
||||||
|
elif tool_name in ("character_update", "world_update"):
|
||||||
|
desc = "updating the records"
|
||||||
|
elif tool_name == "journal_update":
|
||||||
|
desc = "updating the journal"
|
||||||
elif tool_name == "roll":
|
elif tool_name == "roll":
|
||||||
dice = (args or {}).get("dice", "1d6")
|
dice = (args or {}).get("dice", "1d6")
|
||||||
mod = (args or {}).get("modifier")
|
mod = (args or {}).get("modifier")
|
||||||
@ -526,6 +599,12 @@ class GameEngine:
|
|||||||
"read_file": self._tool_read_file,
|
"read_file": self._tool_read_file,
|
||||||
"roll": self._tool_roll,
|
"roll": self._tool_roll,
|
||||||
"think": self._tool_think,
|
"think": self._tool_think,
|
||||||
|
"character_get": self._tool_character_get,
|
||||||
|
"character_update": self._tool_character_update,
|
||||||
|
"world_get": self._tool_world_get,
|
||||||
|
"world_update": self._tool_world_update,
|
||||||
|
"journal_get": self._tool_journal_get,
|
||||||
|
"journal_update": self._tool_journal_update,
|
||||||
}
|
}
|
||||||
fn = fn_map.get(tool_name)
|
fn = fn_map.get(tool_name)
|
||||||
if not fn:
|
if not fn:
|
||||||
@ -664,10 +743,6 @@ class GameEngine:
|
|||||||
book_log=args.get("book_log", ""),
|
book_log=args.get("book_log", ""),
|
||||||
user_prompt=args.get("user_prompt", ""),
|
user_prompt=args.get("user_prompt", ""),
|
||||||
ambience=args.get("ambience"),
|
ambience=args.get("ambience"),
|
||||||
character_updates=args.get("character_updates"),
|
|
||||||
world_updates=args.get("world_updates"),
|
|
||||||
journal_add=args.get("journal_add", []),
|
|
||||||
journal_done=args.get("journal_done", []),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute other tools
|
# Execute other tools
|
||||||
@ -797,25 +872,9 @@ class GameEngine:
|
|||||||
|
|
||||||
def apply_state(self, result: TurnResult) -> None:
|
def apply_state(self, result: TurnResult) -> None:
|
||||||
"""Write state changes from a TurnResult to disk."""
|
"""Write state changes from a TurnResult to disk."""
|
||||||
|
|
||||||
if result.character_updates and self._validate_update_size(
|
|
||||||
"character", result.character_updates, CHAR_PATH
|
|
||||||
):
|
|
||||||
CHAR_PATH.write_text(result.character_updates.strip() + "\n")
|
|
||||||
|
|
||||||
if result.world_updates and self._validate_update_size(
|
|
||||||
"world", result.world_updates, WORLD_PATH
|
|
||||||
):
|
|
||||||
WORLD_PATH.write_text(result.world_updates.strip() + "\n")
|
|
||||||
|
|
||||||
if result.ambience:
|
if result.ambience:
|
||||||
AMBIENCE_PATH.write_text(result.ambience.strip().lower() + "\n")
|
AMBIENCE_PATH.write_text(result.ambience.strip().lower() + "\n")
|
||||||
|
|
||||||
if result.journal_add or result.journal_done:
|
|
||||||
self._update_journal(
|
|
||||||
add=result.journal_add, done=result.journal_done
|
|
||||||
)
|
|
||||||
|
|
||||||
def archive_turn(self, narrative: str) -> None:
|
def archive_turn(self, narrative: str) -> None:
|
||||||
"""Append the narrative as a new turn in book.md."""
|
"""Append the narrative as a new turn in book.md."""
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
@ -840,54 +899,68 @@ class GameEngine:
|
|||||||
if not JOURNAL_PATH.exists():
|
if not JOURNAL_PATH.exists():
|
||||||
JOURNAL_PATH.write_text("# Journal\n\n## TODO\n\n## DONE\n\n")
|
JOURNAL_PATH.write_text("# Journal\n\n## TODO\n\n## DONE\n\n")
|
||||||
lines = JOURNAL_PATH.read_text().splitlines()
|
lines = JOURNAL_PATH.read_text().splitlines()
|
||||||
new_lines = []
|
|
||||||
in_todo = False
|
# Parse into sections: everything before TODO, TODO items, between, DONE items, after
|
||||||
in_done = False
|
todo_items: list[str] = []
|
||||||
|
done_items: list[str] = []
|
||||||
|
before_todo: list[str] = []
|
||||||
|
between: list[str] = []
|
||||||
|
after_done: list[str] = []
|
||||||
|
section = "before_todo"
|
||||||
for line in lines:
|
for line in lines:
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith("## TODO"):
|
|
||||||
in_todo = True
|
|
||||||
in_done = False
|
|
||||||
elif stripped.startswith("## DONE"):
|
|
||||||
in_todo = False
|
|
||||||
in_done = True
|
|
||||||
new_lines.append(line)
|
|
||||||
|
|
||||||
# Find insertion points
|
|
||||||
todo_idx = None
|
|
||||||
done_idx = None
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
if stripped == "## TODO":
|
if stripped == "## TODO":
|
||||||
todo_idx = i
|
section = "todo"
|
||||||
|
before_todo.append(line)
|
||||||
elif stripped == "## DONE":
|
elif stripped == "## DONE":
|
||||||
done_idx = i
|
section = "done"
|
||||||
|
between.append(line)
|
||||||
|
elif section == "before_todo":
|
||||||
|
before_todo.append(line)
|
||||||
|
elif section == "todo":
|
||||||
|
if stripped.startswith("- "):
|
||||||
|
todo_items.append(stripped[2:])
|
||||||
|
else:
|
||||||
|
between.append(line)
|
||||||
|
elif section == "done":
|
||||||
|
if stripped.startswith("- "):
|
||||||
|
done_items.append(stripped[2:])
|
||||||
|
else:
|
||||||
|
after_done.append(line)
|
||||||
|
|
||||||
|
# Apply changes
|
||||||
if done:
|
if done:
|
||||||
for item in done:
|
done_set = set(done)
|
||||||
# Remove from TODO if present
|
todo_items = [i for i in todo_items if i not in done_set]
|
||||||
new_lines = [
|
new_done = [i for i in done if i not in done_items]
|
||||||
l for l in new_lines
|
done_items.extend(new_done)
|
||||||
if l.strip().lstrip("- ").lstrip("☐ ") != item
|
|
||||||
]
|
|
||||||
# Find DONE section and add
|
|
||||||
if done_idx is not None:
|
|
||||||
done_entry = f"- {item}"
|
|
||||||
if done_idx + 1 < len(new_lines):
|
|
||||||
new_lines.insert(done_idx + 1, done_entry)
|
|
||||||
else:
|
|
||||||
new_lines.append(done_entry)
|
|
||||||
|
|
||||||
if add:
|
if add:
|
||||||
for item in add:
|
todo_set = set(todo_items)
|
||||||
entry = f"- {item}"
|
new_todo = [i for i in add if i not in todo_set]
|
||||||
if entry not in new_lines:
|
# Insert new items at the top of TODO
|
||||||
if todo_idx is not None:
|
todo_items = new_todo + todo_items
|
||||||
new_lines.insert(todo_idx + 1, entry)
|
|
||||||
else:
|
|
||||||
new_lines.append(entry)
|
|
||||||
|
|
||||||
JOURNAL_PATH.write_text("\n".join(new_lines) + "\n")
|
# Reconstruct
|
||||||
|
out = list(before_todo)
|
||||||
|
for item in todo_items:
|
||||||
|
out.append(f"- {item}")
|
||||||
|
out.extend(between)
|
||||||
|
for item in done_items:
|
||||||
|
out.append(f"- {item}")
|
||||||
|
out.extend(after_done)
|
||||||
|
|
||||||
|
# Clean up: collapse multiple blank lines
|
||||||
|
cleaned = []
|
||||||
|
prev_blank = False
|
||||||
|
for line in out:
|
||||||
|
is_blank = line.strip() == ""
|
||||||
|
if is_blank and prev_blank:
|
||||||
|
continue
|
||||||
|
cleaned.append(line)
|
||||||
|
prev_blank = is_blank
|
||||||
|
# Ensure trailing newline
|
||||||
|
JOURNAL_PATH.write_text("\n".join(cleaned) + "\n")
|
||||||
|
|
||||||
|
|
||||||
# ── CLI entry point (for testing) ─────────────────────────────────────────
|
# ── CLI entry point (for testing) ─────────────────────────────────────────
|
||||||
|
|||||||
28
tools/run.py
28
tools/run.py
@ -44,6 +44,7 @@ JOURNAL_PATH = SESSION / 'journal.md'
|
|||||||
AMBIENCE_PATH = SESSION / 'ambience.md'
|
AMBIENCE_PATH = SESSION / 'ambience.md'
|
||||||
AMBIENCE_OPTIONS_PATH = SESSION / 'ambience_options.md'
|
AMBIENCE_OPTIONS_PATH = SESSION / 'ambience_options.md'
|
||||||
BOOK_PATH = SESSION / 'book.md'
|
BOOK_PATH = SESSION / 'book.md'
|
||||||
|
LAST_PROMPT_PATH = SESSION / 'last_prompt.md'
|
||||||
AUDIO_DIR = SESSION / 'audio'
|
AUDIO_DIR = SESSION / 'audio'
|
||||||
TODAY = date.today().isoformat()
|
TODAY = date.today().isoformat()
|
||||||
LOG_PATH = LOG_DIR / f'{TODAY}.md'
|
LOG_PATH = LOG_DIR / f'{TODAY}.md'
|
||||||
@ -596,7 +597,19 @@ class ChaosTUI(App):
|
|||||||
self.call_after_refresh(self._begin_game)
|
self.call_after_refresh(self._begin_game)
|
||||||
|
|
||||||
def _begin_game(self):
|
def _begin_game(self):
|
||||||
"""Generate the first scene of the game."""
|
"""Resume from last saved prompt or generate an opening scene."""
|
||||||
|
if LAST_PROMPT_PATH.exists():
|
||||||
|
saved = LAST_PROMPT_PATH.read_text().strip()
|
||||||
|
if saved:
|
||||||
|
self._last_prompt = saved
|
||||||
|
pages = load_book_pages()
|
||||||
|
parts = []
|
||||||
|
if pages:
|
||||||
|
parts.append(pages[-1])
|
||||||
|
parts.append(f"---\n\n{saved}")
|
||||||
|
self._set_narrative("\n\n".join(parts))
|
||||||
|
self._enable_input()
|
||||||
|
return
|
||||||
self._call_llm()
|
self._call_llm()
|
||||||
|
|
||||||
# ── Ambience ─────────────────────────────────────────
|
# ── Ambience ─────────────────────────────────────────
|
||||||
@ -735,13 +748,22 @@ class ChaosTUI(App):
|
|||||||
# Display the next user prompt
|
# Display the next user prompt
|
||||||
self._display_scene(result)
|
self._display_scene(result)
|
||||||
|
|
||||||
|
# Persist the prompt so the game resumes here on restart
|
||||||
|
if result.user_prompt:
|
||||||
|
LAST_PROMPT_PATH.write_text(result.user_prompt.strip())
|
||||||
|
|
||||||
# Store for next turn
|
# Store for next turn
|
||||||
self._last_prompt = result.user_prompt
|
self._last_prompt = result.user_prompt
|
||||||
self._last_result = result
|
self._last_result = result
|
||||||
|
|
||||||
def _display_scene(self, result: TurnResult) -> None:
|
def _display_scene(self, result: TurnResult) -> None:
|
||||||
"""Update the UI with the next user prompt."""
|
"""Update the UI with the last story entry followed by the DM prompt."""
|
||||||
self._set_narrative(result.user_prompt)
|
parts = []
|
||||||
|
if result.book_log:
|
||||||
|
parts.append(result.book_log)
|
||||||
|
if result.user_prompt:
|
||||||
|
parts.append(f"---\n\n{result.user_prompt}")
|
||||||
|
self._set_narrative("\n\n".join(parts) if parts else "")
|
||||||
self._enable_input()
|
self._enable_input()
|
||||||
|
|
||||||
def _enable_input(self) -> None:
|
def _enable_input(self) -> None:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user