@@ -454,7 +454,13 @@ func (p *Plugin) enterInteractiveMode() tea.Cmd {
454454 target = sessionName // Fall back to session name if pane ID not available
455455 }
456456 if target != "" {
457- previewWidth , previewHeight := p .calculatePreviewDimensions ()
457+ // When terminal panel is visible, agent pane only gets a portion
458+ var previewWidth , previewHeight int
459+ if p .termPanelVisible {
460+ previewWidth , previewHeight = p .calculateAgentPaneDimensions ()
461+ } else {
462+ previewWidth , previewHeight = p .calculatePreviewDimensions ()
463+ }
458464 tty .SetWindowSizeManual (sessionName )
459465 p .resizeTmuxPane (target , previewWidth , previewHeight )
460466 // Verify and retry once if resize didn't take effect
@@ -499,6 +505,49 @@ func (p *Plugin) enterInteractiveMode() tea.Cmd {
499505 return tea .Batch (cmds ... )
500506}
501507
508+ // enterTermPanelInteractiveMode enters interactive mode targeting the terminal panel's tmux session.
509+ func (p * Plugin ) enterTermPanelInteractiveMode () tea.Cmd {
510+ if ! features .IsEnabled (features .TmuxInteractiveInput .Name ) {
511+ return nil
512+ }
513+ if p .termPanelSession == "" || ! p .termPanelVisible {
514+ return nil
515+ }
516+
517+ sessionName := p .termPanelSession
518+ paneID := p .termPanelPaneID
519+ target := paneID
520+ if target == "" {
521+ target = sessionName
522+ }
523+
524+ // Resize terminal panel pane to match its split dimensions
525+ w , h := p .calculateTermPanelDimensions ()
526+ tty .SetWindowSizeManual (sessionName )
527+ p .resizeTmuxPane (target , w , h )
528+ if aw , ah , ok := queryPaneSize (target ); ok && (aw != w || ah != h ) {
529+ p .resizeTmuxPane (target , w , h )
530+ }
531+
532+ p .termPanelScroll = 0 // Reset scroll so output aligns with cursor position
533+ p .interactiveState = & InteractiveState {
534+ Active : true ,
535+ TargetPane : paneID ,
536+ TargetSession : sessionName ,
537+ TermPanel : true ,
538+ LastKeyTime : time .Now (),
539+ CursorVisible : true ,
540+ }
541+ p .selection .Clear ()
542+ p .viewMode = ViewModeInteractive
543+
544+ // Invalidate the background poll chain so only the interactive poll loop runs.
545+ // The interactive chain captures the same pane and updates termPanelOutput,
546+ // so background polling is redundant during interactive mode.
547+ p .termPanelGeneration ++
548+ return p .pollInteractivePane ()
549+ }
550+
502551// calculatePreviewDimensions returns the content width and height for the preview pane.
503552// Used to resize tmux panes to match the visible area.
504553// IMPORTANT: This must stay in sync with renderListView() width calculations.
@@ -580,7 +629,17 @@ func (p *Plugin) resizeTmuxTargetCmd(target string) tea.Cmd {
580629 return nil
581630 }
582631
583- previewWidth , previewHeight := p .calculatePreviewDimensions ()
632+ // Determine dimensions: terminal panel target gets terminal panel dims,
633+ // agent target gets split-aware dims, or full dims if no panel.
634+ var previewWidth , previewHeight int
635+ isTermPanel := p .termPanelVisible && (target == p .termPanelPaneID || target == p .termPanelSession )
636+ if isTermPanel {
637+ previewWidth , previewHeight = p .calculateTermPanelDimensions ()
638+ } else if p .termPanelVisible {
639+ previewWidth , previewHeight = p .calculateAgentPaneDimensions ()
640+ } else {
641+ previewWidth , previewHeight = p .calculatePreviewDimensions ()
642+ }
584643 return func () tea.Msg {
585644 if actualWidth , actualHeight , ok := queryPaneSize (target ); ok {
586645 if actualWidth == previewWidth && actualHeight == previewHeight {
@@ -605,7 +664,15 @@ func (p *Plugin) maybeResizeInteractivePane(paneWidth, paneHeight int) tea.Cmd {
605664 return nil
606665 }
607666
608- previewWidth , previewHeight := p .calculatePreviewDimensions ()
667+ var previewWidth , previewHeight int
668+ isTermPanel := p .interactiveState .TermPanel
669+ if isTermPanel && p .termPanelVisible {
670+ previewWidth , previewHeight = p .calculateTermPanelDimensions ()
671+ } else if p .termPanelVisible {
672+ previewWidth , previewHeight = p .calculateAgentPaneDimensions ()
673+ } else {
674+ previewWidth , previewHeight = p .calculatePreviewDimensions ()
675+ }
609676 if paneWidth == previewWidth && paneHeight == previewHeight {
610677 return nil
611678 }
@@ -743,6 +810,8 @@ func (p *Plugin) previewResizeTarget() string {
743810// exitInteractiveMode exits interactive mode and returns to list view.
744811func (p * Plugin ) exitInteractiveMode () {
745812 if p .interactiveState != nil {
813+ // Preserve focus on whichever sub-pane was interactive
814+ p .termPanelFocused = p .interactiveState .TermPanel
746815 p .interactiveState .Active = false
747816 }
748817 p .interactiveState = nil
@@ -773,10 +842,37 @@ func (p *Plugin) handleInteractiveKeys(msg tea.KeyMsg) tea.Cmd {
773842 return p .pollSelectedAgentNowIfVisible ()
774843 }
775844
845+ // Terminal panel toggle: intercept before forwarding to tmux
846+ if msg .String () == "ctrl+t" {
847+ cmd := p .toggleTermPanel ()
848+ // If interactive mode survived the toggle (agent pane still active),
849+ // keep focus on agent pane and resize the interactive pane.
850+ if p .interactiveState != nil && p .interactiveState .Active && ! p .interactiveState .TermPanel {
851+ p .termPanelFocused = false
852+ return tea .Batch (cmd , p .resizeInteractivePaneCmd ())
853+ }
854+ return cmd
855+ }
856+ if msg .String () == "alt+t" {
857+ cmd := p .switchTermPanelLayout ()
858+ if p .interactiveState != nil && p .interactiveState .Active {
859+ return tea .Batch (cmd , p .resizeInteractivePaneCmd ())
860+ }
861+ return cmd
862+ }
863+
776864 // Attach shortcut: exit interactive and attach to full session (td-fd68d1)
777865 if msg .String () == p .getInteractiveAttachKey () {
866+ isTermPanel := p .interactiveState != nil && p .interactiveState .TermPanel
778867 p .exitInteractiveMode ()
779- // Attach to the appropriate session
868+ // Terminal panel: attach to its tmux session
869+ if isTermPanel && p .termPanelSession != "" {
870+ sessionName := p .termPanelSession
871+ return p .attachWithResize (sessionName , sessionName , "terminal" , func (err error ) tea.Msg {
872+ return TmuxAttachFinishedMsg {Err : err }
873+ })
874+ }
875+ // Attach to the appropriate agent/shell session
780876 if p .shellSelected {
781877 if idx := p .selectedShellIdx ; idx >= 0 && idx < len (p .shells ) {
782878 return p .ensureShellAndAttachByIndex (idx )
@@ -1052,6 +1148,18 @@ func (p *Plugin) forwardScrollToTmux(delta int) tea.Cmd {
10521148 }
10531149 p .lastScrollTime = now
10541150
1151+ // When interactive mode targets the terminal panel, scroll terminal panel output
1152+ if p .interactiveState != nil && p .interactiveState .TermPanel {
1153+ if delta < 0 {
1154+ p .termPanelScroll ++
1155+ } else {
1156+ if p .termPanelScroll > 0 {
1157+ p .termPanelScroll --
1158+ }
1159+ }
1160+ return nil
1161+ }
1162+
10551163 if delta < 0 {
10561164 // Scroll up: pause auto-scroll, show older content
10571165 p .autoScrollOutput = false
@@ -1149,13 +1257,46 @@ func (p *Plugin) interactiveMouseCoords(x, y int) (col, row int, ok bool) {
11491257 }
11501258 contentY ++ // hint line
11511259
1260+ // When interactive mode targets the terminal panel, adjust content origin
1261+ // to account for the terminal panel's position within the preview area.
1262+ targetingTermPanel := p .interactiveState != nil && p .interactiveState .Active && p .interactiveState .TermPanel && p .termPanelVisible
1263+ if targetingTermPanel {
1264+ previewWidth , previewHeight := p .calculatePreviewDimensions ()
1265+ size := p .termPanelEffectiveSize ()
1266+ if p .termPanelLayout == TermPanelRight {
1267+ termWidth := previewWidth * size / 100
1268+ if termWidth < 10 {
1269+ termWidth = 10
1270+ }
1271+ outputWidth := previewWidth - termWidth - 1
1272+ if outputWidth < 10 {
1273+ outputWidth = 10
1274+ }
1275+ contentX += outputWidth + 1 // skip agent output + divider
1276+ } else {
1277+ termHeight := previewHeight * size / 100
1278+ if termHeight < 3 {
1279+ termHeight = 3
1280+ }
1281+ outputHeight := previewHeight - termHeight - 1
1282+ if outputHeight < 3 {
1283+ outputHeight = 3
1284+ }
1285+ contentY += outputHeight + 1 // skip agent output + divider
1286+ }
1287+ contentY ++ // terminal panel hint/label line
1288+ }
1289+
11521290 relX := x - contentX
11531291 relY := y - contentY
11541292 if relX < 0 || relY < 0 {
11551293 return 0 , 0 , false
11561294 }
11571295
11581296 paneWidth , paneHeight := p .calculatePreviewDimensions ()
1297+ if targetingTermPanel {
1298+ paneWidth , paneHeight = p .calculateTermPanelDimensions ()
1299+ }
11591300 if p .interactiveState != nil {
11601301 if p .interactiveState .PaneWidth > 0 && p .interactiveState .PaneWidth < paneWidth {
11611302 paneWidth = p .interactiveState .PaneWidth
@@ -1207,6 +1348,11 @@ func (p *Plugin) pollInteractivePane() tea.Cmd {
12071348 interval = pollingDecayMedium
12081349 }
12091350
1351+ // When interactive mode targets the terminal panel, use terminal panel polling
1352+ if p .interactiveState .TermPanel {
1353+ return p .scheduleTermPanelPoll (interval )
1354+ }
1355+
12101356 // Use existing shell or worktree polling mechanism
12111357 // Worktrees use scheduleInteractivePoll to skip stagger (td-8856c9)
12121358 if p .shellSelected && p .selectedShellIdx >= 0 && p .selectedShellIdx < len (p .shells ) {
@@ -1225,6 +1371,14 @@ func (p *Plugin) scheduleDebouncedPoll(delay time.Duration) tea.Cmd {
12251371 return nil
12261372 }
12271373
1374+ // When interactive mode targets the terminal panel, use terminal panel polling.
1375+ // Increment generation to invalidate stale timers from previous keystrokes,
1376+ // preventing poll chain accumulation during rapid typing.
1377+ if p .interactiveState .TermPanel {
1378+ p .termPanelGeneration ++
1379+ return p .scheduleTermPanelPoll (delay )
1380+ }
1381+
12281382 // Use shell or worktree polling mechanism based on current selection.
12291383 // IMPORTANT: Use the correct generation map for each type (td-97327e):
12301384 // - Shells use shellPollGeneration (checked by scheduleShellPollByName)
@@ -1256,6 +1410,11 @@ func (p *Plugin) pollInteractivePaneImmediate() tea.Cmd {
12561410 return nil
12571411 }
12581412
1413+ // When interactive mode targets the terminal panel, use terminal panel polling
1414+ if p .interactiveState .TermPanel {
1415+ return p .scheduleTermPanelPoll (0 )
1416+ }
1417+
12591418 // Schedule with 0ms delay for immediate capture (td-8856c9: no stagger for worktrees)
12601419 if p .shellSelected && p .selectedShellIdx >= 0 && p .selectedShellIdx < len (p .shells ) {
12611420 return p .scheduleShellPollByName (p .shells [p .selectedShellIdx ].TmuxName , 0 )
0 commit comments