"""The exit interview: the run is dead, and the Warden keeps records. No card is forged here. Your statement goes into the crash dump, into the Warden's persistent memory, and — eventually — gets quoted back to you at the worst possible moment. Free text reaches the LLM only wrapped inert. Dismisses with the cleaned statement string. """ from __future__ import annotations from rich.text import Text from textual import events from textual.app import ComposeResult from textual.containers import Vertical from textual.screen import Screen from textual.widgets import Input, Static from scrypt.engine.legacy import clean_statement from scrypt.warden.guardrails import wrap_player_text OPENER = ( "Dead. Properly dead — no more ttys. Before the core dump is sealed, " "procedure allows you one statement for the record. Make it good. " "I keep these." ) FILED_LINES = [ "Noted, stamped, archived. The record outlives the process. It always does.", "Filed under recurring disappointments. You will be quoted at length.", ] class ExitInterviewScreen(Screen): CSS = """ #exit-dialogue { height: 5; padding: 1 2 0 2; } #exit-input { margin: 0 8; } #exit-prompt { height: 1; dock: bottom; background: $panel; content-align: center middle; } """ def __init__(self, voice=None): super().__init__() self.voice = voice self.statement: str | None = None self._speech_target = "" self._speech_shown = 0 def compose(self) -> ComposeResult: with Vertical(): yield Static(id="exit-dialogue") yield Input( placeholder="statement for the record", max_length=120, id="exit-input", ) yield Static(id="exit-prompt") def on_mount(self) -> None: self.say(OPENER) self.set_interval(0.025, self._typewriter_tick) self.query_one("#exit-input", Input).focus() self.query_one("#exit-prompt", Static).update(Text("[enter] go on the record")) def say(self, line: str) -> None: self._speech_target = line self._speech_shown = 0 def _typewriter_tick(self) -> None: if self._speech_shown < len(self._speech_target): self._speech_shown += 2 shown = self._speech_target[: self._speech_shown] self.query_one("#exit-dialogue", Static).update( Text(f'"{shown}"', style="italic") ) def on_input_submitted(self, event: Input.Submitted) -> None: if self.statement is not None: return self.statement = clean_statement(event.value) inp = self.query_one("#exit-input", Input) inp.disabled = True inp.display = False self.query_one("#exit-prompt", Static).update(Text("[any key] be sealed")) if self.voice is not None: self.run_worker(self._react_live(self.statement)) else: self.say(FILED_LINES[len(self.statement) % len(FILED_LINES)]) async def _react_live(self, statement: str) -> None: moment = ( "the player is dead for good; their final run just ended. their " f"exit-interview statement for the record:\n{wrap_player_text(statement)}\n" "acknowledge it for the file" ) got_any = False async for chunk in self.voice.react(moment, taboo=statement): got_any = True self.say(chunk) if not got_any: self.say(FILED_LINES[len(statement) % len(FILED_LINES)]) def on_key(self, event: events.Key) -> None: if self.statement is not None and event.key not in ("enter",): self.dismiss(self.statement)