mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
feat: implement full alt screen mode with in-memory scrollback
Add ScrollList component for viewport-based message history with lazy rendering and offset-based scrolling. Implement MessageItem system for user, assistant, tool, system, and error messages with pre-rendered styled content from MessageRenderer. Key changes: - ScrollList: height-constrained viewport with itemGap support, padding to ensure fixed height for sticky bottom layout - MessageItem implementations with preRendered content from MessageRenderer - refreshContent() pattern for efficient ScrollList updates - Mouse wheel scrolling (3 lines per tick) with auto-scroll behavior - All message types (user, assistant, tool, system, error, extension) properly added to in-memory scrollback - PgUp/PgDn/Alt+Home/Alt+End keybindings for navigation - Removed tea.Println() calls for alt screen compatibility - Sticky bottom layout: input, separator, status bar fixed at bottom Files added: - internal/ui/scrolllist.go (ScrollList component) - internal/ui/message_items.go (MessageItem implementations) Files modified: - internal/ui/model.go (main integration) - internal/ui/*.go (alt screen config for components)
This commit is contained in:
@@ -533,7 +533,14 @@ func (s *InputComponent) View() tea.View {
|
||||
view.WriteString(helpStyle.Render(hint))
|
||||
}
|
||||
|
||||
return tea.NewView(containerStyle.Render(view.String()))
|
||||
v := tea.NewView(containerStyle.Render(view.String()))
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// renderPopup renders the autocomplete popup for slash command suggestions.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// MessageItem implementations for ScrollList
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TextMessageItem represents a completed text message (user or assistant)
|
||||
// in the scrollback. It uses pre-rendered styled content from MessageRenderer.
|
||||
type TextMessageItem struct {
|
||||
id string
|
||||
role string // "user" or "assistant"
|
||||
content string // Raw content (for re-rendering if needed)
|
||||
preRendered string // Pre-rendered styled content from MessageRenderer
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// NewTextMessageItem creates a new text message for the scrollback.
|
||||
// The content should be pre-rendered using MessageRenderer for proper styling.
|
||||
func NewTextMessageItem(id string, role string, content string) *TextMessageItem {
|
||||
return &TextMessageItem{
|
||||
id: id,
|
||||
role: role,
|
||||
content: content,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewStyledMessageItem creates a message item with pre-rendered styled content.
|
||||
// This is the preferred way to create messages when you have styled content from MessageRenderer.
|
||||
func NewStyledMessageItem(id string, role string, rawContent string, preRendered string) *TextMessageItem {
|
||||
return &TextMessageItem{
|
||||
id: id,
|
||||
role: role,
|
||||
content: rawContent,
|
||||
preRendered: preRendered,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TextMessageItem) ID() string {
|
||||
return m.id
|
||||
}
|
||||
|
||||
func (m *TextMessageItem) Render(width int) string {
|
||||
// If we have pre-rendered styled content, return it
|
||||
if m.preRendered != "" {
|
||||
return m.preRendered
|
||||
}
|
||||
|
||||
// Fallback to simple formatting if no pre-rendered content
|
||||
return m.renderContent(width)
|
||||
}
|
||||
|
||||
func (m *TextMessageItem) Height() int {
|
||||
rendered := m.Render(0) // Width doesn't matter since we use pre-rendered
|
||||
if rendered == "" {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(rendered, "\n") + 1
|
||||
}
|
||||
|
||||
func (m *TextMessageItem) renderContent(width int) string {
|
||||
var parts []string
|
||||
|
||||
// Role indicator
|
||||
if m.role == "user" {
|
||||
parts = append(parts, "│ ▸ You")
|
||||
} else {
|
||||
parts = append(parts, "") // Assistant messages start without role
|
||||
}
|
||||
|
||||
// Content with simple wrapping
|
||||
contentWidth := width - 4
|
||||
if contentWidth < 20 {
|
||||
contentWidth = 20
|
||||
}
|
||||
|
||||
lines := strings.Split(m.content, "\n")
|
||||
for _, line := range lines {
|
||||
if len(line) <= contentWidth {
|
||||
parts = append(parts, "│ "+line)
|
||||
} else {
|
||||
// Basic wrap
|
||||
for len(line) > contentWidth {
|
||||
parts = append(parts, "│ "+line[:contentWidth])
|
||||
line = line[contentWidth:]
|
||||
}
|
||||
if len(line) > 0 {
|
||||
parts = append(parts, "│ "+line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// SystemMessageItem - System messages (commands, info, errors)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// SystemMessageItem represents a system message (commands, info, errors).
|
||||
type SystemMessageItem struct {
|
||||
id string
|
||||
content string
|
||||
timestamp time.Time
|
||||
cachedRender string
|
||||
cachedWidth int
|
||||
}
|
||||
|
||||
// NewSystemMessageItem creates a new system message for the scrollback.
|
||||
func NewSystemMessageItem(id, content string) *SystemMessageItem {
|
||||
return &SystemMessageItem{
|
||||
id: id,
|
||||
content: content,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SystemMessageItem) ID() string {
|
||||
return m.id
|
||||
}
|
||||
|
||||
func (m *SystemMessageItem) Render(width int) string {
|
||||
// Return cached render if width matches
|
||||
if m.cachedWidth == width && m.cachedRender != "" {
|
||||
return m.cachedRender
|
||||
}
|
||||
|
||||
// Simple system message formatting
|
||||
rendered := "│ " + strings.ReplaceAll(m.content, "\n", "\n│ ")
|
||||
|
||||
// Cache and return
|
||||
m.cachedRender = rendered
|
||||
m.cachedWidth = width
|
||||
return rendered
|
||||
}
|
||||
|
||||
func (m *SystemMessageItem) Height() int {
|
||||
if m.cachedRender != "" {
|
||||
return strings.Count(m.cachedRender, "\n") + 1
|
||||
}
|
||||
// Estimate
|
||||
if m.cachedWidth > 0 {
|
||||
return (len(m.content) / max(m.cachedWidth-10, 40)) + 3
|
||||
}
|
||||
return 3
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helper: generateMessageID
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
var messageCounter = 0
|
||||
|
||||
func generateMessageID() string {
|
||||
messageCounter++
|
||||
return fmt.Sprintf("msg-%d-%d", time.Now().UnixNano(), messageCounter)
|
||||
}
|
||||
+278
-51
@@ -441,13 +441,24 @@ type AppModel struct {
|
||||
// badge above the input. Cleared when the steer is consumed.
|
||||
steeringMessages []string
|
||||
|
||||
// scrollList manages the in-memory message history with viewport scrolling.
|
||||
// Replaces the terminal scrollback (tea.Println) pattern with in-memory
|
||||
// scrollback for alt screen mode.
|
||||
scrollList *ScrollList
|
||||
|
||||
// messages holds all completed messages in the conversation history.
|
||||
// The scrollList renders from this slice based on its viewport offset.
|
||||
messages []MessageItem
|
||||
|
||||
// pendingUserPrints holds user messages that have been consumed from the
|
||||
// queue but not yet printed to scrollback. They are deferred until
|
||||
// SpinnerEvent{Show: true} so the previous assistant response can be
|
||||
// flushed first, preserving chronological order.
|
||||
// NOTE: With ScrollList, we add these directly to messages instead of printing.
|
||||
pendingUserPrints []string
|
||||
|
||||
// scrollbackBuf collects rendered content during a single Update() call.
|
||||
// scrollbackBuf is DEPRECATED in alt screen mode but kept for compatibility.
|
||||
// In alt screen mode, messages go directly to the scrollList.
|
||||
// All print helpers append here instead of returning tea.Println directly.
|
||||
// The buffer is drained into a single atomic tea.Println at the end of
|
||||
// each Update call via drainScrollback(). If the stream component has
|
||||
@@ -588,6 +599,10 @@ type AppModel struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
// quitting signals that the app is shutting down. When true, View()
|
||||
// disables alt screen to restore the terminal properly.
|
||||
quitting bool
|
||||
|
||||
// streamingBashOutput holds the current streaming bash output lines.
|
||||
// Lines are accumulated as they arrive and displayed in the stream region.
|
||||
streamingBashOutput []string
|
||||
@@ -715,6 +730,11 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel {
|
||||
// Initialize streaming bash output buffer.
|
||||
m.streamingBashMaxLines = 50 // cap to prevent memory issues
|
||||
|
||||
// Initialize ScrollList for in-memory message history (alt screen mode).
|
||||
// Height will be set properly by distributeHeight().
|
||||
m.scrollList = NewScrollList(width, height-10) // Placeholder height
|
||||
m.messages = []MessageItem{}
|
||||
|
||||
// Wire up child components now that we have the concrete implementations.
|
||||
m.input = NewInputComponent(width, "Enter your prompt (Type /help for commands, Ctrl+C to quit)", appCtrl)
|
||||
|
||||
@@ -994,6 +1014,8 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.layoutDirty = true
|
||||
// Update renderer width for proper message styling
|
||||
m.renderer.SetWidth(m.width)
|
||||
// Propagate to children.
|
||||
if m.input != nil {
|
||||
updated, cmd := m.input.Update(msg)
|
||||
@@ -1006,6 +1028,21 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
// ── Mouse wheel scrolling ────────────────────────────────────────────────
|
||||
case tea.MouseWheelMsg:
|
||||
// Scroll the scrollback viewport with mouse wheel
|
||||
const scrollLines = 3
|
||||
switch msg.Button {
|
||||
case tea.MouseWheelUp:
|
||||
m.scrollList.ScrollBy(-scrollLines)
|
||||
m.scrollList.autoScroll = false
|
||||
case tea.MouseWheelDown:
|
||||
m.scrollList.ScrollBy(scrollLines)
|
||||
if m.scrollList.AtBottom() {
|
||||
m.scrollList.autoScroll = true
|
||||
}
|
||||
}
|
||||
|
||||
// ── Keyboard input ───────────────────────────────────────────────────────
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
@@ -1022,6 +1059,8 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.overlayResponseCh = nil
|
||||
m.overlay = nil
|
||||
}
|
||||
// Set quitting flag so View() disables alt screen for clean exit.
|
||||
m.quitting = true
|
||||
// Graceful quit: app.Close() is deferred in cmd/root.go.
|
||||
return m, tea.Quit
|
||||
}
|
||||
@@ -1041,6 +1080,31 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollback keybindings (PgUp/PgDn/Home/End) for navigating message history.
|
||||
// Only active when not working (to avoid conflicts during streaming).
|
||||
if m.state == stateInput {
|
||||
switch msg.String() {
|
||||
case "pgup":
|
||||
m.scrollList.ScrollBy(-m.scrollList.height)
|
||||
m.scrollList.autoScroll = false
|
||||
return m, tea.Batch(cmds...)
|
||||
case "pgdown":
|
||||
m.scrollList.ScrollBy(m.scrollList.height)
|
||||
if m.scrollList.AtBottom() {
|
||||
m.scrollList.autoScroll = true
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
case "alt+home":
|
||||
m.scrollList.GotoTop()
|
||||
m.scrollList.autoScroll = false
|
||||
return m, tea.Batch(cmds...)
|
||||
case "alt+end":
|
||||
m.scrollList.GotoBottom()
|
||||
m.scrollList.autoScroll = true
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
// Thinking keybindings — only when the model supports reasoning.
|
||||
if m.isReasoningModel {
|
||||
switch msg.String() {
|
||||
@@ -1398,12 +1462,13 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
case app.ResponseCompleteEvent:
|
||||
// This event fires for both streaming and non-streaming paths.
|
||||
// In streaming mode, the content was already delivered via StreamChunkEvents
|
||||
// and is sitting in the stream component (possibly with reasoning). Don't
|
||||
// print or reset — flushStreamContent() handles it on the next step.
|
||||
// In streaming mode, flush the accumulated stream content to scrollback.
|
||||
// In non-streaming mode (no stream content accumulated), print the text.
|
||||
hasStreamContent := m.stream != nil && m.stream.GetRenderedContent() != ""
|
||||
if !hasStreamContent && strings.TrimSpace(msg.Content) != "" {
|
||||
if hasStreamContent {
|
||||
// Flush stream content to scrollback immediately
|
||||
m.flushStreamContent()
|
||||
} else if strings.TrimSpace(msg.Content) != "" {
|
||||
m.printAssistantMessage(msg.Content)
|
||||
if m.stream != nil {
|
||||
m.stream.Reset()
|
||||
@@ -1689,10 +1754,15 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// to see a non-empty scrollbackBuf and trigger its auto-flush, which calls
|
||||
// GetRenderedContent() + Reset() while the stream is still active —
|
||||
// causing duplication and premature resets.
|
||||
//
|
||||
// NOTE: In alt screen mode, overflow is handled differently - we don't use
|
||||
// tea.Println() since that writes to terminal scrollback, not alt screen.
|
||||
// The StreamingMessageItem dynamically renders the current stream content.
|
||||
// Overflow is not emitted - the full stream content is always rendered
|
||||
// via StreamingMessageItem in the ScrollList viewport.
|
||||
if m.stream != nil {
|
||||
if overflow := m.stream.ConsumeOverflow(); overflow != "" {
|
||||
cmds = append(cmds, tea.Println(overflow))
|
||||
}
|
||||
// Consume and discard overflow in alt screen mode
|
||||
_ = m.stream.ConsumeOverflow()
|
||||
}
|
||||
|
||||
cmds = append(cmds, m.drainScrollback())
|
||||
@@ -1704,6 +1774,15 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// The status bar is always present (1 line) to avoid layout shifts.
|
||||
// When the tree selector is active, it replaces the stream region.
|
||||
func (m *AppModel) View() tea.View {
|
||||
// When quitting, disable alt screen for clean terminal restoration.
|
||||
// This prevents terminal corruption issues on exit.
|
||||
if m.quitting {
|
||||
v := tea.NewView("")
|
||||
v.AltScreen = false
|
||||
v.MouseMode = tea.MouseModeNone
|
||||
return v
|
||||
}
|
||||
|
||||
// Tree selector overlay replaces the normal layout.
|
||||
if m.state == stateTreeSelector && m.treeSelector != nil {
|
||||
return m.treeSelector.View()
|
||||
@@ -1721,7 +1800,14 @@ func (m *AppModel) View() tea.View {
|
||||
|
||||
// Overlay dialog replaces the normal layout.
|
||||
if m.state == stateOverlay && m.overlay != nil {
|
||||
return tea.NewView(m.overlay.Render())
|
||||
v := tea.NewView(m.overlay.Render())
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Recompute layout heights if any Update() changed state that affects
|
||||
@@ -1735,7 +1821,8 @@ func (m *AppModel) View() tea.View {
|
||||
|
||||
vis := m.uiVis()
|
||||
|
||||
streamView := m.renderStream()
|
||||
// Render scrollback content from ScrollList (replaces renderStream() in alt screen mode)
|
||||
scrollbackView := m.renderScrollback()
|
||||
|
||||
// Propagate hint visibility to the input component before rendering.
|
||||
if ic, ok := m.input.(*InputComponent); ok {
|
||||
@@ -1760,11 +1847,26 @@ func (m *AppModel) View() tea.View {
|
||||
parts = append(parts, headerView)
|
||||
}
|
||||
|
||||
// Only include the stream region when it has content. When idle the
|
||||
// stream renders "" which JoinVertical would pad to a full-width blank
|
||||
// Only include the scrollback region when it has content. When idle the
|
||||
// scrollback renders "" which JoinVertical would pad to a full-width blank
|
||||
// line, inflating the view unnecessarily.
|
||||
if streamView != "" {
|
||||
parts = append(parts, streamView)
|
||||
if scrollbackView != "" {
|
||||
parts = append(parts, scrollbackView)
|
||||
}
|
||||
|
||||
// Add bash output and canceling warning between scrollback and separator
|
||||
// (these don't go inside scrollback viewport to avoid affecting scroll position)
|
||||
theme := GetTheme()
|
||||
bashView := m.renderStreamingBashOutput(theme)
|
||||
if bashView != "" {
|
||||
parts = append(parts, bashView)
|
||||
}
|
||||
if m.canceling {
|
||||
warning := lipgloss.NewStyle().
|
||||
Foreground(theme.Warning).
|
||||
Bold(true).
|
||||
Render(" ⚠ Press ESC again to cancel")
|
||||
parts = append(parts, warning)
|
||||
}
|
||||
|
||||
if !vis.HideSeparator {
|
||||
@@ -1798,7 +1900,14 @@ func (m *AppModel) View() tea.View {
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
|
||||
return tea.NewView(content)
|
||||
v := tea.NewView(content)
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -1840,6 +1949,31 @@ func (m *AppModel) renderStream() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
}
|
||||
|
||||
// refreshContent updates the ScrollList with current messages.
|
||||
// Called whenever messages change (new message, streaming update, etc.)
|
||||
// ScrollList lazily renders only visible items on View() call.
|
||||
func (m *AppModel) refreshContent() {
|
||||
if m.scrollList == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// MessageItem implements ScrollItem interface, so we can use copy
|
||||
m.scrollList.SetItems(m.messages)
|
||||
|
||||
// Only adjust scroll position if auto-scroll is enabled
|
||||
if m.scrollList.autoScroll {
|
||||
m.scrollList.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
// renderScrollback returns the scrollback content from ScrollList.
|
||||
// This replaces renderStream() in alt screen mode.
|
||||
func (m *AppModel) renderScrollback() string {
|
||||
// Content is refreshed via refreshContent() when messages change
|
||||
// ScrollList renders lazily on View() call
|
||||
return m.scrollList.View()
|
||||
}
|
||||
|
||||
// renderStreamingBashOutput renders accumulated streaming bash output (stdout + stderr)
|
||||
// below the LLM streaming text. Returns empty string if no bash output is present.
|
||||
// Lines are truncated to the terminal width and capped to maxBashLines to prevent
|
||||
@@ -2214,25 +2348,78 @@ func (m *AppModel) renderQueuedMessages() string {
|
||||
|
||||
// printUserMessage renders a user message into the scrollback buffer.
|
||||
func (m *AppModel) printUserMessage(text string) {
|
||||
m.appendScrollback(m.renderer.RenderUserMessage(text, time.Now()).Content)
|
||||
// Check if this exact message was just added (prevents duplicates)
|
||||
if len(m.messages) > 0 {
|
||||
if lastMsg, ok := m.messages[len(m.messages)-1].(*TextMessageItem); ok {
|
||||
if lastMsg.role == "user" && lastMsg.content == text {
|
||||
return // Skip duplicate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render styled content using MessageRenderer
|
||||
styledMsg := m.renderer.RenderUserMessage(text, time.Now())
|
||||
|
||||
// Add to in-memory scrollList with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "user", text, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Refresh ScrollList content and scroll to bottom
|
||||
m.refreshContent()
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
|
||||
// printAssistantMessage renders an assistant message into the scrollback buffer.
|
||||
func (m *AppModel) printAssistantMessage(text string) {
|
||||
if strings.TrimSpace(text) != "" {
|
||||
m.appendScrollback(m.renderer.RenderAssistantMessage(text, time.Now(), m.modelName).Content)
|
||||
// Render styled content using MessageRenderer
|
||||
styledMsg := m.renderer.RenderAssistantMessage(text, time.Now(), m.modelName)
|
||||
|
||||
// Add to in-memory scrollList with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "assistant", text, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Refresh ScrollList content and scroll to bottom
|
||||
m.refreshContent()
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// printToolResult renders a tool result message into the scrollback buffer.
|
||||
func (m *AppModel) printToolResult(evt app.ToolResultEvent) {
|
||||
m.appendScrollback(m.renderer.RenderToolMessage(evt.ToolName, evt.ToolArgs, evt.Result, evt.IsError).Content)
|
||||
// Render styled tool message using MessageRenderer
|
||||
styledMsg := m.renderer.RenderToolMessage(evt.ToolName, evt.ToolArgs, evt.Result, evt.IsError)
|
||||
|
||||
// Add to in-memory scrollList with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "tool", styledMsg.Content, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Refresh ScrollList content
|
||||
m.refreshContent()
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
|
||||
// printErrorResponse renders an error message into the scrollback buffer.
|
||||
func (m *AppModel) printErrorResponse(evt app.StepErrorEvent) {
|
||||
if evt.Err != nil {
|
||||
m.appendScrollback(m.renderer.RenderErrorMessage(evt.Err.Error(), time.Now()).Content)
|
||||
// Render styled error message using MessageRenderer
|
||||
styledMsg := m.renderer.RenderErrorMessage(evt.Err.Error(), time.Now())
|
||||
|
||||
// Add to in-memory scrollList with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "error", styledMsg.Content, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Refresh ScrollList content
|
||||
m.refreshContent()
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2246,6 +2433,7 @@ func (m *AppModel) printErrorResponse(evt app.StepErrorEvent) {
|
||||
func (m *AppModel) handleSlashCommand(sc *SlashCommand, args string) tea.Cmd {
|
||||
switch sc.Name {
|
||||
case "/quit":
|
||||
m.quitting = true
|
||||
return tea.Quit
|
||||
case "/help":
|
||||
m.printHelpMessage()
|
||||
@@ -2305,7 +2493,18 @@ func (m *AppModel) handleSlashCommand(sc *SlashCommand, args string) tea.Cmd {
|
||||
|
||||
// printSystemMessage renders a system-level message into the scrollback buffer.
|
||||
func (m *AppModel) printSystemMessage(text string) {
|
||||
m.appendScrollback(m.renderer.RenderSystemMessage(text, time.Now()).Content)
|
||||
// Render styled system message using MessageRenderer
|
||||
styledMsg := m.renderer.RenderSystemMessage(text, time.Now())
|
||||
|
||||
// Add to in-memory scrollList with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "system", styledMsg.Content, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Refresh ScrollList content
|
||||
m.refreshContent()
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
|
||||
// printExtensionBlock renders a custom styled block from an extension with
|
||||
@@ -2333,6 +2532,15 @@ func (m *AppModel) printExtensionBlock(evt app.ExtensionPrintEvent) {
|
||||
WithBorderColor(borderClr),
|
||||
WithMarginBottom(1),
|
||||
)
|
||||
|
||||
// Add to in-memory scrollList with rendered content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "extension", rendered, rendered)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Refresh ScrollList content
|
||||
m.refreshContent()
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(rendered)
|
||||
}
|
||||
|
||||
@@ -2593,7 +2801,19 @@ func (m *AppModel) flushStreamContent() {
|
||||
return
|
||||
}
|
||||
m.stream.Reset()
|
||||
m.appendScrollback(content)
|
||||
|
||||
// Render styled content using MessageRenderer
|
||||
styledMsg := m.renderer.RenderAssistantMessage(content, time.Now(), m.modelName)
|
||||
|
||||
// Add to in-memory scrollList as a completed assistant message with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "assistant", content, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Refresh ScrollList content
|
||||
m.refreshContent()
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
|
||||
// flushStreamAndPendingUserMessages moves the previous assistant response and
|
||||
@@ -2605,16 +2825,35 @@ func (m *AppModel) flushStreamAndPendingUserMessages() {
|
||||
if m.stream != nil {
|
||||
if content := m.stream.GetRenderedContent(); content != "" {
|
||||
m.stream.Reset()
|
||||
m.appendScrollback(content)
|
||||
|
||||
// Render styled content using MessageRenderer
|
||||
styledMsg := m.renderer.RenderAssistantMessage(content, time.Now(), m.modelName)
|
||||
|
||||
// Add to in-memory scrollList with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "assistant", content, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Render pending user messages from the queue.
|
||||
for _, text := range m.pendingUserPrints {
|
||||
rendered := m.renderer.RenderUserMessage(text, time.Now()).Content
|
||||
m.appendScrollback(rendered)
|
||||
// Render styled content using MessageRenderer
|
||||
styledMsg := m.renderer.RenderUserMessage(text, time.Now())
|
||||
|
||||
// Add to in-memory scrollList with styled content
|
||||
msg := NewStyledMessageItem(generateMessageID(), "user", text, styledMsg.Content)
|
||||
m.messages = append(m.messages, msg)
|
||||
|
||||
// Also append to legacy buffer for compatibility
|
||||
m.appendScrollback(styledMsg.Content)
|
||||
}
|
||||
m.pendingUserPrints = nil
|
||||
|
||||
// Refresh ScrollList content once after all messages are added
|
||||
m.refreshContent()
|
||||
}
|
||||
|
||||
// appendScrollback adds rendered content to the scrollback buffer. The content
|
||||
@@ -2632,34 +2871,16 @@ func (m *AppModel) appendScrollback(content string) {
|
||||
// stream content is flushed a ClearScreen follows to clean up orphaned terminal
|
||||
// rows left after the view height shrinks. Returns nil if there is nothing to
|
||||
// print.
|
||||
//
|
||||
// drainScrollback is a no-op in alt screen mode. Scrollback is managed
|
||||
// in-memory by ScrollList and never printed via tea.Println().
|
||||
// The scrollbackBuf is still populated for compatibility but cleared here
|
||||
// to prevent memory leaks.
|
||||
func (m *AppModel) drainScrollback() tea.Cmd {
|
||||
if len(m.scrollbackBuf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var parts []string
|
||||
needsClear := false
|
||||
|
||||
// Auto-flush any stream content so it appears before new messages.
|
||||
if m.stream != nil {
|
||||
if content := m.stream.GetRenderedContent(); content != "" {
|
||||
m.stream.Reset()
|
||||
parts = append(parts, content)
|
||||
needsClear = true
|
||||
}
|
||||
}
|
||||
|
||||
parts = append(parts, m.scrollbackBuf...)
|
||||
m.scrollbackBuf = m.scrollbackBuf[:0]
|
||||
|
||||
printCmd := tea.Println(strings.Join(parts, "\n"))
|
||||
if needsClear {
|
||||
return tea.Sequence(
|
||||
printCmd,
|
||||
func() tea.Msg { return tea.ClearScreen() },
|
||||
)
|
||||
}
|
||||
return printCmd
|
||||
// In alt screen mode, all scrollback is managed in-memory by ScrollList.
|
||||
// Never use tea.Println() as it writes to terminal scrollback, not alt screen.
|
||||
m.scrollbackBuf = m.scrollbackBuf[:0] // Clear buffer to prevent memory leak
|
||||
return nil
|
||||
}
|
||||
|
||||
// distributeHeight recalculates child component heights after a window resize,
|
||||
@@ -2735,6 +2956,12 @@ func (m *AppModel) distributeHeight() {
|
||||
|
||||
streamHeight := max(m.height-separatorLines-widgetLines-headerFooterLines-queuedLines-inputLines-statusBarLines, 0)
|
||||
|
||||
// In alt screen mode, give the calculated height to ScrollList instead of stream.
|
||||
// The stream component still exists but is embedded as the last item in scrollList.
|
||||
m.scrollList.SetHeight(streamHeight)
|
||||
m.scrollList.SetWidth(m.width)
|
||||
|
||||
// Keep stream height in sync for rendering (even though it's embedded in scrollList)
|
||||
if m.stream != nil {
|
||||
m.stream.SetHeight(streamHeight)
|
||||
}
|
||||
|
||||
@@ -281,7 +281,14 @@ func (ms *ModelSelectorComponent) View() tea.View {
|
||||
footerStyle := lipgloss.NewStyle().Foreground(theme.Muted).PaddingLeft(2)
|
||||
b.WriteString(footerStyle.Render(strings.Join(footerParts, " ")))
|
||||
|
||||
return tea.NewView(b.String())
|
||||
v := tea.NewView(b.String())
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// IsActive returns whether the selector is still accepting input.
|
||||
|
||||
@@ -118,22 +118,33 @@ func (m ProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// status information and help text. Displays error messages if present or
|
||||
// a completion message when the download finishes.
|
||||
func (m ProgressModel) View() tea.View {
|
||||
var v tea.View
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
|
||||
if m.err != nil {
|
||||
return tea.NewView(fmt.Sprintf("Error: %s\n", m.err.Error()))
|
||||
v.Content = fmt.Sprintf("Error: %s\n", m.err.Error())
|
||||
return v
|
||||
}
|
||||
|
||||
if m.complete {
|
||||
return tea.NewView(fmt.Sprintf("\n%s%s\n\n%sComplete!\n",
|
||||
v.Content = fmt.Sprintf("\n%s%s\n\n%sComplete!\n",
|
||||
strings.Repeat(" ", padding),
|
||||
m.progress.View(),
|
||||
strings.Repeat(" ", padding)))
|
||||
strings.Repeat(" ", padding))
|
||||
return v
|
||||
}
|
||||
|
||||
pad := strings.Repeat(" ", padding)
|
||||
return tea.NewView(fmt.Sprintf("\n%s%s\n%s%s\n\n%s",
|
||||
v.Content = fmt.Sprintf("\n%s%s\n%s%s\n\n%s",
|
||||
pad, m.progress.View(),
|
||||
pad, m.status,
|
||||
pad+helpStyle("Press 'q' or Ctrl+C to cancel")))
|
||||
pad+helpStyle("Press 'q' or Ctrl+C to cancel"))
|
||||
return v
|
||||
}
|
||||
|
||||
// ProgressReader wraps an io.Reader to intercept and parse Ollama pull operation
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MessageItem is the interface all scrollback messages must implement.
|
||||
// This allows lazy rendering - messages are only rendered when visible.
|
||||
type MessageItem interface {
|
||||
// Render returns the styled content for this message at the given width.
|
||||
// Implementations should cache the result to avoid re-rendering.
|
||||
Render(width int) string
|
||||
|
||||
// Height returns the number of lines this message occupies when rendered.
|
||||
Height() int
|
||||
|
||||
// ID returns a unique identifier for this message (for tracking).
|
||||
ID() string
|
||||
}
|
||||
|
||||
// ScrollList manages a viewport over a list of MessageItems.
|
||||
// It handles offset-based scrolling and lazy rendering. Only visible
|
||||
// items are rendered on each View() call.
|
||||
type ScrollList struct {
|
||||
items []MessageItem
|
||||
offsetIdx int // Index of first visible item
|
||||
offsetLine int // Lines to skip from first visible item
|
||||
width int
|
||||
height int // Viewport height in lines
|
||||
autoScroll bool // Whether to auto-scroll to bottom on new content
|
||||
itemGap int // Number of blank lines between items (0 = no gap)
|
||||
}
|
||||
|
||||
// NewScrollList creates a new ScrollList with the given dimensions.
|
||||
func NewScrollList(width, height int) *ScrollList {
|
||||
return &ScrollList{
|
||||
items: []MessageItem{},
|
||||
offsetIdx: 0,
|
||||
offsetLine: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
autoScroll: true, // Start with auto-scroll enabled
|
||||
}
|
||||
}
|
||||
|
||||
// SetItems replaces the items in the scroll list. If auto-scroll is enabled,
|
||||
// the viewport will scroll to the bottom to show the latest content.
|
||||
func (s *ScrollList) SetItems(items []MessageItem) {
|
||||
s.items = items
|
||||
if s.autoScroll {
|
||||
s.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
// SetHeight updates the viewport height. Called when the terminal is resized.
|
||||
func (s *ScrollList) SetHeight(height int) {
|
||||
s.height = height
|
||||
s.clampOffset()
|
||||
}
|
||||
|
||||
// SetWidth updates the viewport width. Called when the terminal is resized.
|
||||
// This may invalidate cached renders in MessageItems.
|
||||
func (s *ScrollList) SetWidth(width int) {
|
||||
s.width = width
|
||||
s.clampOffset()
|
||||
}
|
||||
|
||||
// SetItemGap sets the number of blank lines between items (0 = no gap).
|
||||
func (s *ScrollList) SetItemGap(gap int) {
|
||||
s.itemGap = gap
|
||||
}
|
||||
|
||||
// ItemGap returns the current gap between items.
|
||||
func (s *ScrollList) ItemGap() int {
|
||||
return s.itemGap
|
||||
}
|
||||
|
||||
// ScrollBy scrolls the viewport by the given number of lines.
|
||||
// Positive = scroll down, negative = scroll up.
|
||||
func (s *ScrollList) ScrollBy(lines int) {
|
||||
if lines > 0 {
|
||||
// Scroll down
|
||||
for lines > 0 && s.offsetIdx < len(s.items) {
|
||||
if s.offsetIdx >= len(s.items) {
|
||||
break
|
||||
}
|
||||
currentItem := s.items[s.offsetIdx]
|
||||
itemHeight := currentItem.Height()
|
||||
remainingLines := itemHeight - s.offsetLine
|
||||
|
||||
if lines >= remainingLines {
|
||||
// Move to next item
|
||||
s.offsetIdx++
|
||||
s.offsetLine = 0
|
||||
lines -= remainingLines
|
||||
// Consume gap lines between items
|
||||
if s.itemGap > 0 && s.offsetIdx < len(s.items) {
|
||||
if lines >= s.itemGap {
|
||||
lines -= s.itemGap
|
||||
} else {
|
||||
lines = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stay on current item, skip more lines
|
||||
s.offsetLine += lines
|
||||
lines = 0
|
||||
}
|
||||
}
|
||||
} else if lines < 0 {
|
||||
// Scroll up
|
||||
lines = -lines
|
||||
for lines > 0 && (s.offsetIdx > 0 || s.offsetLine > 0) {
|
||||
if s.offsetLine > 0 {
|
||||
// Scroll within current item
|
||||
if lines >= s.offsetLine {
|
||||
lines -= s.offsetLine
|
||||
s.offsetLine = 0
|
||||
} else {
|
||||
s.offsetLine -= lines
|
||||
lines = 0
|
||||
}
|
||||
} else if s.offsetIdx > 0 {
|
||||
// Consume gap lines between items
|
||||
if s.itemGap > 0 {
|
||||
if lines > s.itemGap {
|
||||
lines -= s.itemGap
|
||||
} else {
|
||||
lines = 0
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Move to previous item
|
||||
s.offsetIdx--
|
||||
if s.offsetIdx < len(s.items) {
|
||||
currentItem := s.items[s.offsetIdx]
|
||||
itemHeight := currentItem.Height()
|
||||
|
||||
if lines >= itemHeight {
|
||||
lines -= itemHeight
|
||||
s.offsetLine = 0
|
||||
} else {
|
||||
s.offsetLine = itemHeight - lines
|
||||
lines = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.clampOffset()
|
||||
}
|
||||
|
||||
// GotoBottom scrolls to the end of the list.
|
||||
func (s *ScrollList) GotoBottom() {
|
||||
if len(s.items) == 0 {
|
||||
s.offsetIdx = 0
|
||||
s.offsetLine = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate total height including gaps
|
||||
totalHeight := 0
|
||||
for i, item := range s.items {
|
||||
totalHeight += item.Height()
|
||||
// Add gap after each item except the last
|
||||
if s.itemGap > 0 && i < len(s.items)-1 {
|
||||
totalHeight += s.itemGap
|
||||
}
|
||||
}
|
||||
|
||||
// If content fits in viewport, start at top
|
||||
if totalHeight <= s.height {
|
||||
s.offsetIdx = 0
|
||||
s.offsetLine = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, position viewport at bottom
|
||||
remaining := totalHeight - s.height
|
||||
for idx := 0; idx < len(s.items); idx++ {
|
||||
itemHeight := s.items[idx].Height()
|
||||
if remaining < itemHeight {
|
||||
s.offsetIdx = idx
|
||||
s.offsetLine = remaining
|
||||
return
|
||||
}
|
||||
remaining -= itemHeight
|
||||
// Subtract gap after item (except last)
|
||||
if s.itemGap > 0 && idx < len(s.items)-1 {
|
||||
remaining -= s.itemGap
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: show last item
|
||||
s.offsetIdx = max(0, len(s.items)-1)
|
||||
s.offsetLine = 0
|
||||
}
|
||||
|
||||
// GotoTop scrolls to the beginning of the list.
|
||||
func (s *ScrollList) GotoTop() {
|
||||
s.offsetIdx = 0
|
||||
s.offsetLine = 0
|
||||
}
|
||||
|
||||
// AtBottom returns true if the viewport is at the bottom of the list.
|
||||
func (s *ScrollList) AtBottom() bool {
|
||||
if len(s.items) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Calculate visible height from current position including gaps
|
||||
visibleHeight := 0
|
||||
for idx := s.offsetIdx; idx < len(s.items); idx++ {
|
||||
item := s.items[idx]
|
||||
itemHeight := item.Height()
|
||||
|
||||
if idx == s.offsetIdx {
|
||||
visibleHeight += itemHeight - s.offsetLine
|
||||
} else {
|
||||
visibleHeight += itemHeight
|
||||
}
|
||||
|
||||
// Add gap after item (except last)
|
||||
if s.itemGap > 0 && idx < len(s.items)-1 {
|
||||
visibleHeight += s.itemGap
|
||||
}
|
||||
|
||||
if visibleHeight >= s.height {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AtTop returns true if the viewport is at the top of the list.
|
||||
func (s *ScrollList) AtTop() bool {
|
||||
return s.offsetIdx == 0 && s.offsetLine == 0
|
||||
}
|
||||
|
||||
// View renders the visible portion of the scrollback.
|
||||
// Only items that fit within the viewport height are rendered.
|
||||
// ALWAYS returns exactly s.height lines (padded with empty lines if needed)
|
||||
// to ensure the input/footer stay fixed at the bottom.
|
||||
func (s *ScrollList) View() string {
|
||||
if s.height <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var lines []string
|
||||
remainingHeight := s.height
|
||||
|
||||
// Render visible items
|
||||
if len(s.items) > 0 {
|
||||
for idx := s.offsetIdx; idx < len(s.items) && remainingHeight > 0; idx++ {
|
||||
item := s.items[idx]
|
||||
content := item.Render(s.width)
|
||||
contentLines := strings.Split(content, "\n")
|
||||
|
||||
startLine := 0
|
||||
if idx == s.offsetIdx {
|
||||
startLine = s.offsetLine
|
||||
}
|
||||
|
||||
for i := startLine; i < len(contentLines) && remainingHeight > 0; i++ {
|
||||
lines = append(lines, contentLines[i])
|
||||
remainingHeight--
|
||||
}
|
||||
|
||||
// Add gap lines between items (but not after the last visible item)
|
||||
if remainingHeight > 0 && idx < len(s.items)-1 && s.itemGap > 0 {
|
||||
for g := 0; g < s.itemGap && remainingHeight > 0; g++ {
|
||||
lines = append(lines, "")
|
||||
remainingHeight--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pad with empty lines to ensure exactly s.height lines
|
||||
// This keeps the input/footer fixed at the bottom of the screen
|
||||
for remainingHeight > 0 {
|
||||
lines = append(lines, "")
|
||||
remainingHeight--
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// ScrollPercent returns the current scroll position as a percentage (0.0-1.0).
|
||||
// 0.0 = at top, 1.0 = at bottom. Useful for scroll indicators.
|
||||
func (s *ScrollList) ScrollPercent() float64 {
|
||||
if len(s.items) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
totalHeight := 0
|
||||
for _, item := range s.items {
|
||||
totalHeight += item.Height()
|
||||
}
|
||||
|
||||
if totalHeight <= s.height {
|
||||
return 1.0 // All content fits, consider it "at bottom"
|
||||
}
|
||||
|
||||
// Calculate how many lines are above the viewport
|
||||
linesAbove := 0
|
||||
for i := 0; i < s.offsetIdx && i < len(s.items); i++ {
|
||||
linesAbove += s.items[i].Height()
|
||||
}
|
||||
linesAbove += s.offsetLine
|
||||
|
||||
scrollableHeight := totalHeight - s.height
|
||||
if scrollableHeight <= 0 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
percent := float64(linesAbove) / float64(scrollableHeight)
|
||||
if percent > 1.0 {
|
||||
percent = 1.0
|
||||
}
|
||||
if percent < 0.0 {
|
||||
percent = 0.0
|
||||
}
|
||||
return percent
|
||||
}
|
||||
|
||||
// clampOffset ensures the offset values are within valid bounds after
|
||||
// resizing or scrolling operations.
|
||||
func (s *ScrollList) clampOffset() {
|
||||
if len(s.items) == 0 {
|
||||
s.offsetIdx = 0
|
||||
s.offsetLine = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Clamp offsetIdx
|
||||
if s.offsetIdx >= len(s.items) {
|
||||
s.offsetIdx = len(s.items) - 1
|
||||
}
|
||||
if s.offsetIdx < 0 {
|
||||
s.offsetIdx = 0
|
||||
}
|
||||
|
||||
// Clamp offsetLine
|
||||
if s.offsetIdx < len(s.items) {
|
||||
itemHeight := s.items[s.offsetIdx].Height()
|
||||
if s.offsetLine >= itemHeight {
|
||||
s.offsetLine = max(0, itemHeight-1)
|
||||
}
|
||||
}
|
||||
if s.offsetLine < 0 {
|
||||
s.offsetLine = 0
|
||||
}
|
||||
}
|
||||
@@ -325,7 +325,14 @@ func (ss *SessionSelectorComponent) View() tea.View {
|
||||
}
|
||||
}
|
||||
|
||||
return tea.NewView(b.String())
|
||||
v := tea.NewView(b.String())
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// IsActive returns whether the selector is still accepting input.
|
||||
|
||||
@@ -563,7 +563,14 @@ func (s *StreamComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (s *StreamComponent) View() tea.View {
|
||||
fullContent := s.render()
|
||||
visibleContent := s.viewContent(fullContent)
|
||||
return tea.NewView(visibleContent)
|
||||
v := tea.NewView(visibleContent)
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
@@ -83,9 +83,19 @@ func (t *ToolApprovalInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (t *ToolApprovalInput) View() tea.View {
|
||||
if t.done {
|
||||
return tea.NewView("we are done")
|
||||
v := tea.NewView("")
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
|
||||
if t.done {
|
||||
v.Content = "we are done"
|
||||
return v
|
||||
}
|
||||
|
||||
containerStyle := lipgloss.NewStyle()
|
||||
|
||||
theme := GetTheme()
|
||||
@@ -135,5 +145,6 @@ func (t *ToolApprovalInput) View() tea.View {
|
||||
}
|
||||
view.WriteString(yesText + "/" + noText + "\n")
|
||||
|
||||
return tea.NewView(containerStyle.Render(inputBoxStyle.Render(view.String())))
|
||||
v.Content = containerStyle.Render(inputBoxStyle.Render(view.String()))
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -265,7 +265,14 @@ func (ts *TreeSelectorComponent) View() tea.View {
|
||||
footer := fmt.Sprintf("(%d/%d) [%s]", ts.cursor+1, len(ts.flatNodes), ts.filter)
|
||||
b.WriteString(footerStyle.Render(footer))
|
||||
|
||||
return tea.NewView(b.String())
|
||||
v := tea.NewView(b.String())
|
||||
v.AltScreen = true
|
||||
v.MouseMode = tea.MouseModeCellMotion
|
||||
v.ReportFocus = true
|
||||
v.KeyboardEnhancements = tea.KeyboardEnhancements{
|
||||
ReportEventTypes: true,
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// IsActive returns whether the tree selector is still accepting input.
|
||||
|
||||
Reference in New Issue
Block a user