feat(tui-go): interactive clarification question view

Render stage clarification questions as an AskUserQuestion-style form:
header chip, prompt, selectable option chips, and a free-text custom slot.
Arrow/hjkl nav, space to select, e for custom, enter to submit. Submit
guards on all-answered and sends ClarificationResponse; the parked stage
re-runs with the answers in context.
This commit is contained in:
2026-06-14 13:00:59 +04:00
parent 6242b590e4
commit 218a98034e
8 changed files with 584 additions and 3 deletions
+35 -2
View File
@@ -12,6 +12,7 @@ const (
StateIdle DisplayState = iota
StateInSession
StateApproval
StateClarification
)
// InputMode toggles what a submitted line targets.
@@ -107,6 +108,23 @@ type Approval struct {
Rationale []string
}
// ClarQuestion is one open question a stage raised (mirrors the wire DTO).
type ClarQuestion struct {
ID string
Prompt string
Header string
Options []string
MultiSelect bool
}
// Clarification is a stage's pending set of open questions for the operator.
type Clarification struct {
RequestID string
SessionID string
StageID string
Questions []ClarQuestion
}
// Session is the UI's view of one server session.
type Session struct {
ID string
@@ -122,7 +140,8 @@ type Session struct {
ToolsByStage map[string][]ManifestTool
Events []EventEntry
Pending *Approval
Active bool // an inference/tool is in flight (drives the spinner)
Clar *Clarification // open questions awaiting answers (clarification view)
Active bool // an inference/tool is in flight (drives the spinner)
}
// Workflow is a launchable workflow advertised by the server.
@@ -229,6 +248,14 @@ type Model struct {
// approval steering input buffer
steerBuffer string
steering bool
// clarification view (the interactive question form)
clarFocus int // focused question index
clarCursor int // option cursor in the focused question (== len(opts) → custom slot)
clarChosen []map[int]bool // per-question selected option indices
clarText []string // per-question free-text / custom answer
clarTyping bool // typing into the custom buffer for the focused question
clarDismissed bool // peeked away from the form (it can be reopened)
}
// NewModel builds the initial idle state.
@@ -257,10 +284,16 @@ func (m Model) displayState() DisplayState {
if m.selectedID == "" || !m.sessionEntered {
return StateIdle
}
s := m.session(m.selectedID)
// A stage's open questions take precedence: the run is parked on them, and they
// must be answered (or peeked away) before anything else makes sense.
if s != nil && s.Clar != nil && !m.clarDismissed {
return StateClarification
}
if m.approvalDismissed {
return StateInSession
}
if s := m.session(m.selectedID); s != nil && s.Pending != nil {
if s != nil && s.Pending != nil {
return StateApproval
}
return StateInSession