File size: 3,743 Bytes
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""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)