@@ -613,8 +613,10 @@ func (m Model) viewInline() tea.View {
613613 b .WriteByte ('\n' )
614614 }
615615
616- // Inline BioComp component (rendered as a block above the composer).
617- if m .activeComponent != nil && m .activeComponent .Component .Mode () == "inline" {
616+ // BioComp component (rendered as a block above the composer).
617+ // In inline TUI mode, all components render inline regardless of their
618+ // declared mode — there is no overlay layer to composite onto.
619+ if m .activeComponent != nil {
618620 comp := m .activeComponent .Component
619621 inlineView , panicked := safeView (comp , m .width , m .height )
620622 if panicked {
@@ -760,21 +762,35 @@ func (m Model) handleProtocol(msg protocolMsg) (tea.Model, tea.Cmd) {
760762 return m , waitForProtocolMsg (m .handler )
761763 }
762764
765+ if dp .Summary == "interrupt" {
766+ // HITL interrupt: flush streamed text to scrollback so the
767+ // supervisor's question is visible while the component renders.
768+ // Do NOT clear tool feed or reset streaming state — the tool
769+ // is still logically running (paused at interrupt).
770+ m .flushStreamBuffer ()
771+ toPrint := m .collectLastAssistantMessage ()
772+ m .rebuildViewportWithMode (true )
773+ printCmd := m .inlinePrintMessagesCmd (toPrint )
774+ return m , tea .Batch (waitForProtocolMsg (m .handler ), printCmd )
775+ }
776+
763777 // Flush any remaining streamed text into typed blocks, then append
764778 // buffered handoff lines that belong directly beneath the response.
765779 m .flushStreamBuffer ()
766780
767- toAppend := make ([]ChatMessage , 0 , len (m .pendingHandoffs ))
781+ // Collect the finalized assistant message for printing. In inline
782+ // flow mode, flushStreamBuffer puts text into m.messages in-place
783+ // but nothing prints it to scrollback — we must do that explicitly.
784+ toPrint := m .collectLastAssistantMessage ()
768785 if len (m .pendingHandoffs ) > 0 {
769- toAppend = append (toAppend , m .pendingHandoffs ... )
786+ m .messages = append (m .messages , m .pendingHandoffs ... )
787+ toPrint = append (toPrint , m .pendingHandoffs ... )
770788 m .pendingHandoffs = m .pendingHandoffs [:0 ]
771789 }
772790 m .isStreaming = false
773791 m .isCanceling = false
774- printCmd := m .appendMessages (toAppend , true )
775- if len (toAppend ) == 0 {
776- m .rebuildViewportWithMode (true )
777- }
792+ m .rebuildViewportWithMode (true )
793+ printCmd := m .inlinePrintMessagesCmd (toPrint )
778794 return m , tea .Batch (waitForProtocolMsg (m .handler ), printCmd )
779795
780796 case protocol .TypeStatus :
@@ -1029,6 +1045,16 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
10291045 return m , mouseCaptureCmd (m .mouseCapture )
10301046 }
10311047
1048+ // Global keys that must ALWAYS work, even with an active component.
1049+ if msg .String () == "ctrl+c" && m .activeComponent != nil {
1050+ m .sendComponentResponse (m .activeComponent .MsgID , "cancel" , map [string ]any {"reason" : "ctrl+c" })
1051+ m .activeComponent = nil
1052+ m .pendingChangeEvent = nil
1053+ m .changeDebounceActive = false
1054+ m .recalculateViewportHeight ()
1055+ // Fall through to the normal ctrl+c handler below.
1056+ }
1057+
10321058 // Intercept keys for active BioComp overlay component.
10331059 if m .activeComponent != nil && m .activeComponent .Component .Mode () == "overlay" {
10341060 result , panicked := safeHandleMsg (m .activeComponent .Component , msg )
@@ -1912,17 +1938,25 @@ func (m Model) layoutReservedRowsLegacy() int {
19121938 if tf := renderToolFeed (m .toolFeed , m .styles , m .width , m .inline ); tf != "" {
19131939 rows += lineCount (tf )
19141940 }
1915- if m .activeComponent != nil && m .activeComponent .Component .Mode () == "inline" {
1916- inlineView := m .activeComponent .Component .View (m .width , m .height )
1917- if inlineView != "" {
1918- rows += lineCount (inlineView )
1941+ if m .activeComponent != nil {
1942+ if m .inline {
1943+ // In inline mode, all components render as inline blocks.
1944+ inlineView := m .activeComponent .Component .View (m .width , m .height )
1945+ if inlineView != "" {
1946+ rows += lineCount (inlineView )
1947+ }
1948+ } else if m .activeComponent .Component .Mode () == "inline" {
1949+ inlineView := m .activeComponent .Component .View (m .width , m .height )
1950+ if inlineView != "" {
1951+ rows += lineCount (inlineView )
1952+ }
19191953 }
19201954 }
19211955 if m .progressActive {
19221956 rows += lineCount (renderProgressBar (m .progressLabel , m .progressCurrent , m .progressTotal , m .width , m .styles ))
19231957 }
19241958
1925- if m .activeComponent != nil && m .activeComponent .Component .Mode () == "overlay" {
1959+ if m .activeComponent != nil && m .activeComponent .Component .Mode () == "overlay" && ! m . inline {
19261960 _ , oh := biocomp .OverlaySize (m .width , m .height , "small" )
19271961 rows += oh
19281962 } else if m .pendingConfirm != nil {
@@ -2048,9 +2082,25 @@ func (m *Model) recalculateComposerDimensions() {
20482082func (m * Model ) recalculateComposerHeight () {
20492083 height := m .composerHeightForValue (m .input .Value ())
20502084 if m .input .Height () == height {
2085+ // Height unchanged, but check if content fits and scroll is off.
2086+ if height > 0 && m .input .ScrollYOffset () > 0 {
2087+ totalVisual := m .composerHeightForValue (m .input .Value ())
2088+ if totalVisual <= height {
2089+ m .input .MoveToBegin ()
2090+ m .input .MoveToEnd ()
2091+ }
2092+ }
20512093 return
20522094 }
20532095 m .input .SetHeight (height )
2096+ // When all content fits in the new height, reset scroll to show
2097+ // everything from the top. MoveToBegin zeroes the viewport offset;
2098+ // MoveToEnd restores the cursor to where the user is typing.
2099+ totalVisual := m .composerHeightForValue (m .input .Value ())
2100+ if totalVisual <= height && m .input .ScrollYOffset () > 0 {
2101+ m .input .MoveToBegin ()
2102+ m .input .MoveToEnd ()
2103+ }
20542104}
20552105
20562106func (m Model ) composerHeightForValue (value string ) int {
0 commit comments