Diff
1diff --git a/internal/game/orchestrator.go b/internal/game/orchestrator.go
2index da7bb0972ec0340a61a00739c91c55c33d0a628d..dc981bfc1b6a8154154712280fc67f57d075866f 100644
3--- a/internal/game/orchestrator.go
4+++ b/internal/game/orchestrator.go
5@@ -110,7 +110,7 @@ func (o *Orchestrator) ActionTurn(ctx context.Context, action string) TurnResult
6 res.SpeakErr = err
7 }
8 }
9- o.state.RecordDisplay(displayFromReply(reply, llm.JudgeResult{}, false))
10+ o.state.RecordDisplay(displayFromReply(action, reply, llm.JudgeResult{}, false))
11 return res
12 }
13
14@@ -170,7 +170,7 @@ func (o *Orchestrator) FinishSpeak(ctx context.Context) TurnResult {
15 if jerr == nil {
16 judge = jres
17 }
18- o.state.RecordDisplay(displayFromReply(reply, judge, jerr == nil))
19+ o.state.RecordDisplay(displayFromReply("", reply, judge, jerr == nil))
20 return res
21 }
22
23@@ -217,8 +217,8 @@ func (o *Orchestrator) compactOpenIfOverBudget(ctx context.Context) error {
24 return nil
25 }
26
27-func displayFromReply(r llm.GameReply, j llm.JudgeResult, hasJudge bool) DisplayEntry {
28- e := DisplayEntry{Desc: r.Desc, Romaji: r.Romaji, English: r.English, HasSpeech: hasSpeech(r)}
29+func displayFromReply(action string, r llm.GameReply, j llm.JudgeResult, hasJudge bool) DisplayEntry {
30+ e := DisplayEntry{Action: action, Desc: r.Desc, Romaji: r.Romaji, English: r.English, HasSpeech: hasSpeech(r)}
31 if hasJudge {
32 e.HasJudge = true
33 e.Score = j.Score
34diff --git a/internal/game/state.go b/internal/game/state.go
35index a8cf93ce2077b4417488cf77c1ab9a02607517c9..db0875d55f0c8617d16b593a1bec5f4b21ea314e 100644
36--- a/internal/game/state.go
37+++ b/internal/game/state.go
38@@ -32,10 +32,12 @@ type Segment struct {
39 Summary string
40 }
41
42-// DisplayEntry is one turn as shown in the adventure log. Speech fields are
43-// empty and HasSpeech false for pure narration turns. ShowRomaji and
44+// DisplayEntry is one turn as shown in the adventure log. Action holds the
45+// player's typed action text; it stays empty for spoken turns. Speech fields
46+// are empty and HasSpeech false for pure narration turns. ShowRomaji and
47 // ShowEnglish are independent: either can be revealed in either order.
48 type DisplayEntry struct {
49+ Action string
50 Desc string
51 Romaji string
52 English string
53diff --git a/internal/ui/app.go b/internal/ui/app.go
54index a978e0613643a5783b50308cf71760ba951b6806..b8ea853342f0d7343aef2c2ef1a191ed9e00b14a 100644
55--- a/internal/ui/app.go
56+++ b/internal/ui/app.go
57@@ -13,6 +13,10 @@ import (
58 // tickInterval refreshes the recording elapsed indicator while a capture is open.
59 const tickInterval = 200 * time.Millisecond
60
61+// openingAction is the scene-setting action fired once at startup so the log
62+// shows where the player begins before any typed input.
63+const openingAction = "Look around."
64+
65 type phase int
66
67 const (
68@@ -51,7 +55,10 @@ func NewModel(state *game.State, orch *game.Orchestrator, recordCapSec int) *mod
69 }
70 }
71
72-func (m *model) Init() tea.Cmd { return nil }
73+func (m *model) Init() tea.Cmd {
74+ m.phase = phaseProcessing
75+ return actionTurn(m.orch, openingAction)
76+}
77
78 func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
79 var cmd tea.Cmd
80@@ -103,6 +110,10 @@ func (m *model) handleKey(k tea.KeyMsg) tea.Cmd {
81 if m.phase != phaseRecording {
82 m.input += string(k.Runes)
83 }
84+ case tea.KeySpace:
85+ if m.phase != phaseRecording {
86+ m.input += " "
87+ }
88 }
89 return nil
90 }
91@@ -160,7 +171,10 @@ func turnStatus(res game.TurnResult) string {
92
93 func (m *model) View() string {
94 var logLines []string
95- for _, e := range m.state.Display() {
96+ for i, e := range m.state.Display() {
97+ if i > 0 {
98+ logLines = append(logLines, "")
99+ }
100 logLines = append(logLines, entryLines(e)...)
101 }
102 vs := viewState{
103@@ -190,6 +204,9 @@ func (m *model) phaseWord() string {
104 // that produced it.
105 func entryLines(e game.DisplayEntry) []string {
106 var lines []string
107+ if e.Action != "" {
108+ lines = append(lines, "> "+sanitizeRomajiOnly(e.Action))
109+ }
110 if e.Desc != "" {
111 lines = append(lines, sanitizeRomajiOnly(e.Desc))
112 }