f599502319a607a63292b13a2243b5d9c83925ef

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

UI: echo typed inputs, opening scene, space key

- Adventure log prints the player's typed action with a '> ' prefix before each world response, with blank lines between turn blocks.
- Fire an opening 'Look around.' turn at startup so the log shows the starting location and surroundings.
- Fix space bar not registering in the input line (handle tea.KeySpace).

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 	}