feat(tui-go): workflow-propose choice panel

Render a workflow.proposed frame as a single-select picker with a manual-answer
slot: ↑↓/jk to choose, enter to launch, e for custom, esc to peek away. Picking
a candidate sends StartSession (and focuses the launched session); the custom
slot continues the conversation via ChatInput. Reuses the modal helpers from the
clarification view.
This commit is contained in:
2026-06-14 13:29:34 +04:00
parent b62b6e09a1
commit b563d9841d
9 changed files with 420 additions and 0 deletions
+29
View File
@@ -13,6 +13,7 @@ const (
StateInSession
StateApproval
StateClarification
StateWorkflowPropose
)
// InputMode toggles what a submitted line targets.
@@ -125,6 +126,23 @@ type Clarification struct {
Questions []ClarQuestion
}
// ProposeCandidate is one workflow the router suggests (mirrors the wire DTO).
type ProposeCandidate struct {
WorkflowID string
Reason string
}
// Proposal is the router's triage suggestion of candidate workflows for a chat
// request, rendered as a choice panel with a manual-answer slot. Picking a candidate
// launches that workflow; the custom slot continues the conversation.
type Proposal struct {
ProposalID string
SessionID string
Prompt string
OriginalRequest string
Candidates []ProposeCandidate
}
// Session is the UI's view of one server session.
type Session struct {
ID string
@@ -141,6 +159,7 @@ type Session struct {
Events []EventEntry
Pending *Approval
Clar *Clarification // open questions awaiting answers (clarification view)
Propose *Proposal // router workflow suggestion awaiting a pick (propose view)
Active bool // an inference/tool is in flight (drives the spinner)
}
@@ -256,6 +275,12 @@ type Model struct {
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)
// workflow-propose view (the router's candidate-workflow picker)
proposeCursor int // cursor over candidates (== len(candidates) → custom slot)
proposeText string // free-text answer typed into the custom slot
proposeTyping bool // typing into the custom buffer
proposeDismissed bool // peeked away from the picker (it can be reopened)
}
// NewModel builds the initial idle state.
@@ -290,6 +315,10 @@ func (m Model) displayState() DisplayState {
if s != nil && s.Clar != nil && !m.clarDismissed {
return StateClarification
}
// A router workflow proposal is the operator's call to make before chatting on.
if s != nil && s.Propose != nil && !m.proposeDismissed {
return StateWorkflowPropose
}
if m.approvalDismissed {
return StateInSession
}