diff --git a/frontend/src/index.css b/frontend/src/index.css
index 66ef541..a3f33f4 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -392,8 +392,17 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
.turn-buttons button { font-size: 0.78rem; padding: 4px 12px; color: var(--text-dim); }
.turn-buttons button:hover:not(:disabled) { color: var(--text); }
-.input-bar { display: flex; gap: 8px; align-items: stretch; }
+.input-bar { display: flex; gap: 8px; align-items: flex-end; }
.input-bar input { flex: 1; }
+/* Auto-growing action box: starts at one line, grows to ~4, then scrolls. */
+.input-bar textarea {
+ flex: 1;
+ resize: none;
+ min-height: 0;
+ max-height: calc(6em + 20px); /* ~4 lines (1.5em each) + padding/border */
+ overflow-y: auto;
+ line-height: 1.5;
+}
.mode-select { display: flex; }
.mode-select button {
diff --git a/frontend/src/pages/Play.jsx b/frontend/src/pages/Play.jsx
index 7bdac83..64c3b0a 100644
--- a/frontend/src/pages/Play.jsx
+++ b/frontend/src/pages/Play.jsx
@@ -612,6 +612,16 @@ export default function Play() {
const storyEndRef = useRef(null)
const abortRef = useRef(null)
const pinnedRef = useRef(true) // autoscroll only while the reader is at the bottom
+ const inputRef = useRef(null)
+
+ // Grow the action box with its content (CSS caps it at ~4 lines, then
+ // scrolls); shrinks back after send() clears the text.
+ useEffect(() => {
+ const el = inputRef.current
+ if (!el) return
+ el.style.height = 'auto'
+ el.style.height = `${el.scrollHeight}px`
+ }, [input])
useEffect(() => {
api.getAdventure(id)
@@ -863,8 +873,9 @@ export default function Play() {
))}
- setInput(e.target.value)}
- onKeyDown={(e) => { if (e.key === 'Enter' && !busy) send() }}
+ // Enter sends; Shift+Enter inserts a newline.
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey && !busy) { e.preventDefault(); send() }
+ }}
/>
{busy ? (