Summary
A kind: "text" question card (the free-form input added in PR #113 / amicode#245) renders its question text but no visible text input. The user cannot answer and must dismiss the card. Choice questions in the same session work normally.
Approach
Fix three compounding failures in the text-card renderer path — each small, all in one PR: (1) CSS visibility: give the text-form its own visible idle state using design-system tokens; (2) container measurement: eliminate the race between resizeInput and the optionsHeight ResizeObserver; (3) focus routing: when kind === "text", bypass the option/customRef focus path and target the textarea directly. Together these guarantee the input is visible, sized, and focusable on mount.
Approaches Considered
- All-three fix (chosen) — addresses the root causes end-to-end; each is a few lines in co-located files; they compound into a single symptom so shipping them together is coherent.
- CSS-only — smallest diff but leaves the measurement race and focus issues latent; may still fail on slower machines or constrained viewports.
- Rewrite text-card as a standalone component — clean separation but over-engineered for the bug; larger refactor risk, delays the fix.
Scope
In: session-question-dock.tsx (focus routing, measurement sync), message-part.css (new [data-slot="question-text-form"] rule). Out: the choice-card renderer (unchanged), the TUI/CLI text-card paths (reported working), schema/tool definition (correct as-is).
Root-cause analysis
The text-card rendering path exists (session-question-dock.tsx:601-631) and was merged in PR #113 (commit cf4f06b, 2026-08-03). Three independent failures compound into the "no visible input" symptom:
1. No CSS for the text-form wrapper (visibility)
File: packages/session-ui/src/components/message-part.css
The textarea lives inside <form data-slot="question-text-form">, which has zero CSS rules. The textarea itself uses data-slot="question-custom-input" — styled for the choice card's secondary "type your own answer" input:
[data-slot="question-custom-input"] {
background: transparent;
border: 0;
box-shadow: none;
/* visible only via placeholder + :focus-visible outline */
}
This is appropriate when the input is a secondary affordance below visible option buttons. But for kind: "text", the textarea is the only affordance. Transparent + borderless = invisible to the user.
2. Container measurement race (sizing)
File: packages/app/src/pages/session/composer/session-question-dock.tsx:208-213, 425-428, 430-434
The question-options container constrains children with:
style={{ "max-height": `${store.optionsHeight * (1 - hidden())}px` }}
optionsHeight is set by a ResizeObserver on optionsRef.scrollHeight (line 211). The textarea's height is set by resizeInput (height→0→scrollHeight) inside focusCustom's setTimeout(0) (line 431-434). Race condition: the ResizeObserver can fire before the setTimeout resolves, recording a container height that doesn't include the textarea's final dimensions.
Initial optionsHeight is 180px (line 84), which is adequate for a single-row textarea — but if the observer fires during the height=0 transient, the Math.max(height, scrollHeight) logic captures the textarea at zero and the container stays at 180 (fine) or, on a re-render, the observer sees the empty form and reports less.
3. Focus routing misses the textarea (interaction)
File: packages/app/src/pages/session/composer/session-question-dock.tsx:160-180
On mount, pickFocus() returns 0 (no options to scan). focus(0) at line 178 routes to:
const el = next === options().length ? customRef : optsRef[next]
With options().length === 0 and next === 0: 0 === 0 → true → targets customRef. But customRef is the "Type your own answer" button from the choice-card path, which does not render for text-kind questions (questionCustomRow returns false). So customRef is undefined; el?.focus() is a no-op.
The textarea has its own focusCustom ref callback that does independent focus via setTimeout — this works if the textarea has non-zero rendered dimensions. But if the container clips it or it's invisible, the browser may not give it focus.
Reproduction
- Start a session that triggers a
kind: "text" question (e.g. the overture's name/affiliation ask, or any interview step that passes kind: "text" in the tool call).
- Observe: the question card header appears, but no visible input affordance below it.
- Expected: a bordered textarea with placeholder text, auto-focused, ready for typing.
Repro payload shape:
{
"questions": [{
"question": "What is your name?",
"header": "Your name",
"kind": "text",
"options": []
}]
}
Acceptance Criteria
- A
kind: "text" question renders a visible textarea: it has a border, background tint or other affordance indicating "type here" in its idle (unfocused) state, following the design-system tokens (--v2-border-border-base, --radius-md, appropriate padding from the 4px grid).
- The textarea receives keyboard focus on mount — the user can type immediately without clicking.
- The textarea has non-zero height on mount (at least one row visible) regardless of content.
- Submitting non-empty text resolves the question (existing behavior, must not regress).
- Choice questions (
kind: "choice" or absent) continue to render option buttons with the "Type your own answer" row per their existing behavior.
- Both light and dark themes — the text-form border/tint is visible and meets contrast requirements in both.
Fix direction (code pointers)
CSS — new rule in message-part.css (~line 1100)
Add a dedicated rule for the text-form when it's the primary input:
[data-slot="question-text-form"] {
padding: 4px 8px; /* --space-1 --space-2 */
}
[data-slot="question-text-form"] > [data-slot="question-custom-input"] {
border: 1px solid var(--v2-border-border-base);
border-radius: var(--radius-md);
padding: 8px 12px; /* --space-2 --space-3 */
background: var(--v2-background-bg-layer-02);
min-height: 36px;
&:focus-visible {
border-color: var(--v2-border-border-focus);
outline: none; /* border IS the ring for this input */
}
}
This scopes the visible treatment to text-form only — the choice card's custom input stays transparent.
Measurement — session-question-dock.tsx (~line 430)
After resizeInput, notify the container:
const focusCustom = (el: HTMLTextAreaElement) => {
setTimeout(() => {
el.focus()
resizeInput(el)
// Sync the container measurement after the textarea has its final height
if (optionsRef) {
setStore("optionsHeight", (h) => Math.max(h, optionsRef!.scrollHeight))
}
}, 0)
}
Focus — session-question-dock.tsx (~line 171)
Short-circuit the focus routing for text-kind questions:
const focus = (i: number) => {
// Text-card: the textarea is the only focusable; skip option routing
if (text()) return // focusCustom handles it via ref callback
const next = clamp(i)
setStore("focus", next)
...
}
Or keep a dedicated textRef and route to it explicitly.
Context
Diagnostics (from original filing)
Summary
A
kind: "text"question card (the free-form input added in PR #113 / amicode#245) renders its question text but no visible text input. The user cannot answer and must dismiss the card. Choice questions in the same session work normally.Approach
Fix three compounding failures in the text-card renderer path — each small, all in one PR: (1) CSS visibility: give the text-form its own visible idle state using design-system tokens; (2) container measurement: eliminate the race between
resizeInputand theoptionsHeightResizeObserver; (3) focus routing: whenkind === "text", bypass the option/customRef focus path and target the textarea directly. Together these guarantee the input is visible, sized, and focusable on mount.Approaches Considered
Scope
In:
session-question-dock.tsx(focus routing, measurement sync),message-part.css(new[data-slot="question-text-form"]rule). Out: the choice-card renderer (unchanged), the TUI/CLI text-card paths (reported working), schema/tool definition (correct as-is).Root-cause analysis
The text-card rendering path exists (
session-question-dock.tsx:601-631) and was merged in PR #113 (commitcf4f06b, 2026-08-03). Three independent failures compound into the "no visible input" symptom:1. No CSS for the text-form wrapper (visibility)
File:
packages/session-ui/src/components/message-part.cssThe textarea lives inside
<form data-slot="question-text-form">, which has zero CSS rules. The textarea itself usesdata-slot="question-custom-input"— styled for the choice card's secondary "type your own answer" input:This is appropriate when the input is a secondary affordance below visible option buttons. But for
kind: "text", the textarea is the only affordance. Transparent + borderless = invisible to the user.2. Container measurement race (sizing)
File:
packages/app/src/pages/session/composer/session-question-dock.tsx:208-213, 425-428, 430-434The
question-optionscontainer constrains children with:optionsHeightis set by a ResizeObserver onoptionsRef.scrollHeight(line 211). The textarea's height is set byresizeInput(height→0→scrollHeight) insidefocusCustom'ssetTimeout(0)(line 431-434). Race condition: the ResizeObserver can fire before the setTimeout resolves, recording a container height that doesn't include the textarea's final dimensions.Initial
optionsHeightis 180px (line 84), which is adequate for a single-row textarea — but if the observer fires during the height=0 transient, theMath.max(height, scrollHeight)logic captures the textarea at zero and the container stays at 180 (fine) or, on a re-render, the observer sees the empty form and reports less.3. Focus routing misses the textarea (interaction)
File:
packages/app/src/pages/session/composer/session-question-dock.tsx:160-180On mount,
pickFocus()returns 0 (no options to scan).focus(0)at line 178 routes to:With
options().length === 0andnext === 0:0 === 0→ true → targetscustomRef. ButcustomRefis the "Type your own answer" button from the choice-card path, which does not render for text-kind questions (questionCustomRowreturns false). SocustomRefis undefined;el?.focus()is a no-op.The textarea has its own
focusCustomref callback that does independent focus via setTimeout — this works if the textarea has non-zero rendered dimensions. But if the container clips it or it's invisible, the browser may not give it focus.Reproduction
kind: "text"question (e.g. the overture's name/affiliation ask, or any interview step that passeskind: "text"in the tool call).Repro payload shape:
{ "questions": [{ "question": "What is your name?", "header": "Your name", "kind": "text", "options": [] }] }Acceptance Criteria
kind: "text"question renders a visible textarea: it has a border, background tint or other affordance indicating "type here" in its idle (unfocused) state, following the design-system tokens (--v2-border-border-base,--radius-md, appropriate padding from the 4px grid).kind: "choice"or absent) continue to render option buttons with the "Type your own answer" row per their existing behavior.Fix direction (code pointers)
CSS — new rule in
message-part.css(~line 1100)Add a dedicated rule for the text-form when it's the primary input:
This scopes the visible treatment to text-form only — the choice card's custom input stays transparent.
Measurement —
session-question-dock.tsx(~line 430)After
resizeInput, notify the container:Focus —
session-question-dock.tsx(~line 171)Short-circuit the focus routing for text-kind questions:
Or keep a dedicated
textRefand route to it explicitly.Context
cf4f06b)amico/issue-245-text-card-questions)inputfield to question tool options for inline text input anomalyco/opencode#30028). Bug is fork-side.Diagnostics (from original filing)
anomalyco/opencode;sst/opencoderedirects but the search index does not follow renames): free-form question input does not exist upstream — open feature request [FEATURE]: Addinputfield to question tool options for inline text input anomalyco/opencode#30028. Bug is in the fork-side addition.