From 9ffafac35c683912a19034f76a777b32a4e18b48 Mon Sep 17 00:00:00 2001 From: creatang Date: Tue, 14 Apr 2026 08:42:57 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor=EF=BC=88tui=EF=BC=89=EF=BC=9APhase?= =?UTF-8?q?=201:=20=E5=B8=83=E5=B1=80=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tui/core/app/app.go | 16 +-- internal/tui/core/app/commands.go | 7 +- internal/tui/core/app/styles.go | 154 ++++++++++++++------------- internal/tui/core/app/update.go | 58 ++++------ internal/tui/core/app/update_test.go | 14 +-- internal/tui/core/app/view.go | 80 +------------- internal/tui/core/app/view_test.go | 2 +- 7 files changed, 121 insertions(+), 210 deletions(-) diff --git a/internal/tui/core/app/app.go b/internal/tui/core/app/app.go index 2c25ac13..4885ac67 100644 --- a/internal/tui/core/app/app.go +++ b/internal/tui/core/app/app.go @@ -181,24 +181,24 @@ func newApp(container tuibootstrap.Container) (App, error) { return "> " }) input.FocusedStyle.Base = lipgloss.NewStyle() - input.FocusedStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(colorUser)).Bold(true) - input.FocusedStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(colorText)) - input.FocusedStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(colorSubtle)) + input.FocusedStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)).Bold(true) + input.FocusedStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)) + input.FocusedStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(oliveGray)) input.FocusedStyle.CursorLine = lipgloss.NewStyle() input.FocusedStyle.CursorLineNumber = lipgloss.NewStyle() input.BlurredStyle.Base = lipgloss.NewStyle() - input.BlurredStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(colorUser)).Bold(true) - input.BlurredStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(colorText)) - input.BlurredStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(colorSubtle)) + input.BlurredStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)).Bold(true) + input.BlurredStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)) + input.BlurredStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(oliveGray)) input.BlurredStyle.CursorLine = lipgloss.NewStyle() input.BlurredStyle.CursorLineNumber = lipgloss.NewStyle() - input.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorUser)) + input.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(terracotta)) input.SetHeight(composerMinHeight) input.Focus() spin := spinner.New() spin.Spinner = spinner.Line - spin.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorPrimary)) + spin.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(terracotta)) h := help.New() h.ShowAll = false diff --git a/internal/tui/core/app/commands.go b/internal/tui/core/app/commands.go index 4dba1c6f..47ff7d2b 100644 --- a/internal/tui/core/app/commands.go +++ b/internal/tui/core/app/commands.go @@ -52,11 +52,8 @@ const ( filePickerTitle = "Browse Files" filePickerSubtitle = "Navigate folders, Enter choose file, Esc cancel" - sidebarTitle = "Sessions" - sidebarFilterHint = "Type / to search" - sidebarOpenHint = "Enter to open" - activityTitle = "Activity" - activitySubtitle = "Latest execution events" + activityTitle = "Activity" + activitySubtitle = "Latest execution events" draftSessionTitle = "Draft" emptyConversationText = "No conversation yet.\nAsk NeoCode to inspect or change code, or type /help to browse local commands." diff --git a/internal/tui/core/app/styles.go b/internal/tui/core/app/styles.go index 83c44a47..85d6c48c 100644 --- a/internal/tui/core/app/styles.go +++ b/internal/tui/core/app/styles.go @@ -7,19 +7,32 @@ import ( ) const ( - colorPrimary = "#7AA2F7" - colorUser = "#4FD6BE" - colorBorder = "#2B3342" - colorError = "#F7768E" - colorSuccess = "#73DACA" - colorText = "#DCE3F0" - colorSubtle = "#7E8AA3" - colorBg = "#090C12" - colorPanel = "#11161F" - colorPanelAlt = "#151C27" - colorCode = "#0E131B" - colorInk = "#081018" - colorWarning = "#E0AF68" + parchment = "#f5f4ed" + ivory = "#faf9f5" + pureWhite = "#ffffff" + warmSand = "#e8e6dc" + darkSurface = "#30302e" + deepDark = "#141413" + + anthropicNearBlack = "#141413" + terracotta = "#c96442" + coralAccent = "#d97757" + + errorCrimson = "#b53333" + focusBlue = "#3898ec" + + charcoalWarm = "#4d4c48" + oliveGray = "#5e5d59" + stoneGray = "#87867f" + darkWarm = "#3d3d3a" + warmSilver = "#b0aea5" + + borderCream = "#f0eee6" + borderWarm = "#e8e6dc" + borderDark = "#30302e" + ringWarm = "#d1cfc5" + ringSubtle = "#dedc01" + ringDeep = "#c2c0b6" ) type styles struct { @@ -48,10 +61,8 @@ type styles struct { streamContent lipgloss.Style messageUserTag lipgloss.Style messageAgentTag lipgloss.Style - messageToolTag lipgloss.Style messageBody lipgloss.Style messageUserBody lipgloss.Style - messageToolBody lipgloss.Style inlineNotice lipgloss.Style inlineError lipgloss.Style inlineSystem lipgloss.Style @@ -79,7 +90,7 @@ type styles struct { func newStyles() styles { panel := lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorBorder)). + BorderForeground(lipgloss.Color(borderWarm)). UnsetBackground(). Padding(0, 1) @@ -87,107 +98,100 @@ func newStyles() styles { doc: lipgloss.NewStyle(). Padding(1, 2, 0, 2). UnsetBackground(). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), headerBar: lipgloss.NewStyle(). UnsetBackground(). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), headerBrand: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(colorPrimary)). + Foreground(lipgloss.Color(terracotta)). UnsetBackground(). Padding(0, 1), headerLabel: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)), + Foreground(lipgloss.Color(oliveGray)), headerPath: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), headerSub: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)), + Foreground(lipgloss.Color(oliveGray)), headerMeta: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), headerSpacer: lipgloss.NewStyle(). Width(1). UnsetBackground(), panel: panel, panelFocused: panel.Copy(). - BorderForeground(lipgloss.Color(colorPrimary)), + BorderForeground(lipgloss.Color(terracotta)), panelTitle: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), panelSubtitle: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)), + Foreground(lipgloss.Color(oliveGray)), panelBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)). + Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(), empty: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)). + Foreground(lipgloss.Color(stoneGray)). Padding(1, 0), sessionRow: lipgloss.NewStyle(). Padding(0, 1). - Foreground(lipgloss.Color(colorText)). + Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(), sessionRowActive: lipgloss.NewStyle(). Padding(0, 1). - Foreground(lipgloss.Color(colorText)). + Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(), sessionRowFocused: lipgloss.NewStyle(). Padding(0, 1). - Foreground(lipgloss.Color(colorText)). + Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(). Bold(true), sessionMeta: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)), + Foreground(lipgloss.Color(oliveGray)), sessionMetaActive: lipgloss.NewStyle(). - Foreground(lipgloss.Color("#94A0B8")), + Foreground(lipgloss.Color(anthropicNearBlack)), sessionMetaFocus: lipgloss.NewStyle(). - Foreground(lipgloss.Color("#AFC3FF")), + Foreground(lipgloss.Color(terracotta)), streamTitle: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), streamMeta: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)), + Foreground(lipgloss.Color(oliveGray)), streamContent: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)). + Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(), - messageUserTag: tagStyle(colorUser), - messageAgentTag: tagStyle(colorPrimary), - messageToolTag: tagStyle(colorSuccess), + messageUserTag: tagStyle(charcoalWarm), + messageAgentTag: tagStyle(terracotta), messageBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)). + Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorBorder)). + BorderForeground(lipgloss.Color(borderWarm)). Padding(0, 0), messageUserBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)). + Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorUser)). - Padding(0, 0), - messageToolBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSuccess)). - UnsetBackground(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorSuccess)). + BorderForeground(lipgloss.Color(terracotta)). Padding(0, 0), inlineNotice: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)). + Foreground(lipgloss.Color(oliveGray)). UnsetBackground(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorBorder)). + BorderForeground(lipgloss.Color(borderWarm)). Padding(0, 0). Italic(true), inlineError: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorError)). + Foreground(lipgloss.Color(errorCrimson)). UnsetBackground(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorError)). + BorderForeground(lipgloss.Color(errorCrimson)). Padding(0, 0). Bold(true), inlineSystem: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)). + Foreground(lipgloss.Color(oliveGray)). UnsetBackground(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorBorder)). + BorderForeground(lipgloss.Color(borderWarm)). Padding(0, 0), codeBlock: lipgloss.NewStyle(). MarginLeft(1). @@ -195,50 +199,50 @@ func newStyles() styles { UnsetBackground(). Border(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorPrimary)), + BorderForeground(lipgloss.Color(borderDark)), codeText: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(warmSilver)), codeCopyButton: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(colorPrimary)). + Foreground(lipgloss.Color(coralAccent)). Underline(true), commandMenu: lipgloss.NewStyle(). UnsetBackground(). Padding(1, 1), commandMenuTitle: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(colorPrimary)), + Foreground(lipgloss.Color(terracotta)), commandUsage: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), commandUsageMatch: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(colorPrimary)), + Foreground(lipgloss.Color(terracotta)), commandDesc: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)), + Foreground(lipgloss.Color(oliveGray)), inputPrefix: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorUser)). + Foreground(lipgloss.Color(terracotta)). Bold(true), inputLine: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorText)), + Foreground(lipgloss.Color(anthropicNearBlack)), inputBox: lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorBorder)). + BorderForeground(lipgloss.Color(borderWarm)). UnsetBackground(). Padding(0, 1), inputBoxFocused: lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(colorPrimary)). + BorderForeground(lipgloss.Color(focusBlue)). UnsetBackground(). Padding(0, 1), footer: lipgloss.NewStyle(). - Foreground(lipgloss.Color(colorSubtle)). + Foreground(lipgloss.Color(oliveGray)). UnsetBackground(), - badgeUser: badge("#12202D", colorUser), - badgeAgent: badge("#16233A", colorPrimary), - badgeSuccess: badge("#102018", colorSuccess), - badgeWarning: badge("#241B10", colorWarning), - badgeError: badge("#26131A", colorError), - badgeMuted: badge(colorPanelAlt, colorSubtle), + badgeUser: badge("", charcoalWarm), + badgeAgent: badge("", coralAccent), + badgeSuccess: badge("", oliveGray), + badgeWarning: badge("", charcoalWarm), + badgeError: badge("", errorCrimson), + badgeMuted: badge("", stoneGray), } } @@ -246,7 +250,7 @@ func tagStyle(fg string) lipgloss.Style { return lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color(fg)). - Background(lipgloss.Color(colorBg)). + UnsetBackground(). Padding(0, 1) } diff --git a/internal/tui/core/app/update.go b/internal/tui/core/app/update.go index c117524f..75fe869d 100644 --- a/internal/tui/core/app/update.go +++ b/internal/tui/core/app/update.go @@ -1276,13 +1276,8 @@ func (a App) transcriptBounds() (int, int, int, int) { streamX := contentX streamY := bodyY - if lay.stacked { - streamY += lay.sidebarHeight - } else { - streamX += lay.sidebarWidth + lay.bodyGap - } - return streamX, streamY, lay.rightWidth, a.transcript.Height + return streamX, streamY, lay.contentWidth, a.transcript.Height } func (a App) isMouseWithinInput(msg tea.MouseMsg) bool { @@ -1302,15 +1297,10 @@ func (a App) inputBounds() (int, int, int, int) { streamX := contentX streamY := bodyY - if lay.stacked { - streamY += lay.sidebarHeight - } else { - streamX += lay.sidebarWidth + lay.bodyGap - } - inputY := streamY + a.transcript.Height + a.activityPreviewHeight() + a.commandMenuHeight(max(24, lay.rightWidth)) - inputHeight := lipgloss.Height(a.renderPrompt(max(24, lay.rightWidth))) - return streamX, inputY, lay.rightWidth, inputHeight + inputY := streamY + a.transcript.Height + a.activityPreviewHeight() + a.commandMenuHeight(lay.contentWidth) + inputHeight := lipgloss.Height(a.renderPrompt(lay.contentWidth)) + return streamX, inputY, lay.contentWidth, inputHeight } func (a App) activityBounds() (int, int, int, int) { @@ -1322,17 +1312,12 @@ func (a App) activityBounds() (int, int, int, int) { streamX := contentX streamY := bodyY - if lay.stacked { - streamY += lay.sidebarHeight - } else { - streamX += lay.sidebarWidth + lay.bodyGap - } activityHeight := a.activityPreviewHeight() if activityHeight <= 0 { - return streamX, streamY + a.transcript.Height, lay.rightWidth, 0 + return streamX, streamY + a.transcript.Height, lay.contentWidth, 0 } - return streamX, streamY + a.transcript.Height, lay.rightWidth, activityHeight + return streamX, streamY + a.transcript.Height, lay.contentWidth, activityHeight } func (a App) isMouseWithinActivity(msg tea.MouseMsg) bool { @@ -1472,16 +1457,11 @@ func (a *App) applyComponentLayout(rebuildTranscript bool) { prevActivityWidth := a.activity.Width prevActivityHeight := a.activity.Height a.help.ShowAll = a.state.ShowHelp - sidebarFrameWidth := a.styles.panelFocused.GetHorizontalFrameSize() - sidebarFrameHeight := a.styles.panelFocused.GetVerticalFrameSize() - sidebarBodyWidth := max(14, lay.sidebarWidth-sidebarFrameWidth) - sidebarBodyHeight := max(4, lay.sidebarHeight-sidebarFrameHeight-lipgloss.Height(a.renderSidebarHeader(sidebarBodyWidth))) - a.sessions.SetSize(sidebarBodyWidth, sidebarBodyHeight) - a.transcript.Width = max(24, lay.rightWidth) + a.transcript.Width = lay.contentWidth a.resizeCommandMenu() - a.input.SetWidth(a.composerInnerWidth(lay.rightWidth)) + a.input.SetWidth(a.composerInnerWidth(lay.contentWidth)) a.input.SetHeight(a.composerHeight()) - transcriptHeight, activityHeight, _, _ := a.waterfallMetrics(a.transcript.Width, lay.rightHeight) + transcriptHeight, activityHeight, _, _ := a.waterfallMetrics(a.transcript.Width, lay.contentHeight) a.transcript.Height = transcriptHeight if activityHeight > 0 { @@ -1489,25 +1469,25 @@ func (a *App) applyComponentLayout(rebuildTranscript bool) { frameHeight := panelStyle.GetVerticalFrameSize() borderWidth := 2 paddingWidth := panelStyle.GetHorizontalFrameSize() - borderWidth - panelWidth := max(1, lay.rightWidth-borderWidth) + panelWidth := max(1, lay.contentWidth-borderWidth) bodyWidth := max(10, panelWidth-paddingWidth) bodyHeight := max(1, activityHeight-frameHeight-1) a.activity.Width = bodyWidth a.activity.Height = bodyHeight } else { - a.activity.Width = max(10, lay.rightWidth-4) + a.activity.Width = max(10, lay.contentWidth-4) a.activity.Height = 0 } - a.providerPicker.SetSize(max(24, tuiutils.Clamp(lay.rightWidth-14, 28, 52)), max(4, tuiutils.Clamp(lay.rightHeight-10, 6, 10))) - a.modelPicker.SetSize(max(24, tuiutils.Clamp(lay.rightWidth-14, 28, 52)), max(4, tuiutils.Clamp(lay.rightHeight-10, 6, 10))) - helpPickerMaxHeight := max(8, lay.rightHeight-6) + a.providerPicker.SetSize(max(24, tuiutils.Clamp(lay.contentWidth-14, 28, 52)), max(4, tuiutils.Clamp(lay.contentHeight-10, 6, 10))) + a.modelPicker.SetSize(max(24, tuiutils.Clamp(lay.contentWidth-14, 28, 52)), max(4, tuiutils.Clamp(lay.contentHeight-10, 6, 10))) + helpPickerMaxHeight := max(8, lay.contentHeight-6) helpPickerDesiredHeight := (len(a.helpPicker.Items()) * 3) + 1 a.helpPicker.SetSize( - max(24, tuiutils.Clamp(lay.rightWidth-14, 28, 52)), + max(24, tuiutils.Clamp(lay.contentWidth-14, 28, 52)), max(6, tuiutils.Clamp(helpPickerDesiredHeight, 6, helpPickerMaxHeight)), ) - a.fileBrowser.SetHeight(max(6, tuiutils.Clamp(lay.rightHeight-8, 8, 16))) + a.fileBrowser.SetHeight(max(6, tuiutils.Clamp(lay.contentHeight-8, 8, 16))) if rebuildTranscript || prevTranscriptWidth != a.transcript.Width { a.rebuildTranscript() } else if a.transcript.AtBottom() || a.isBusy() { @@ -1558,7 +1538,13 @@ func (a *App) rebuildTranscript() { copyButtons := make([]copyCodeButtonBinding, 0, 4) nextCopyID := 1 for _, message := range a.activeMessages { + if message.Role == roleTool { + continue + } rendered, bindings := a.renderMessageBlockWithCopy(message, width, nextCopyID) + if rendered == "" { + continue + } blocks = append(blocks, rendered) copyButtons = append(copyButtons, bindings...) nextCopyID += len(bindings) diff --git a/internal/tui/core/app/update_test.go b/internal/tui/core/app/update_test.go index dfc2e776..661b8c35 100644 --- a/internal/tui/core/app/update_test.go +++ b/internal/tui/core/app/update_test.go @@ -731,21 +731,15 @@ func TestComputeLayoutStackedAndWide(t *testing.T) { app.width = 90 app.height = 40 layout := app.computeLayout() - if !layout.stacked { - t.Fatalf("expected stacked layout for narrow width") - } - if layout.rightWidth <= 0 || layout.sidebarWidth <= 0 { - t.Fatalf("expected positive layout widths, got %+v", layout) + if layout.contentWidth <= 0 { + t.Fatalf("expected positive content width, got %+v", layout) } app.width = 140 app.height = 40 layout = app.computeLayout() - if layout.stacked { - t.Fatalf("expected non-stacked layout for wide width") - } - if layout.rightWidth <= 0 || layout.sidebarWidth <= 0 { - t.Fatalf("expected positive layout widths, got %+v", layout) + if layout.contentWidth <= 0 { + t.Fatalf("expected positive content width, got %+v", layout) } } diff --git a/internal/tui/core/app/view.go b/internal/tui/core/app/view.go index 7247f218..0833c1bc 100644 --- a/internal/tui/core/app/view.go +++ b/internal/tui/core/app/view.go @@ -14,14 +14,8 @@ import ( ) type layout struct { - stacked bool contentWidth int contentHeight int - sidebarWidth int - sidebarHeight int - rightWidth int - rightHeight int - bodyGap int } const headerBarHeight = 1 @@ -29,8 +23,8 @@ const headerBarHeight = 1 func (a App) View() string { docWidth := max(0, a.width-a.styles.doc.GetHorizontalFrameSize()) docHeight := max(0, a.height-a.styles.doc.GetVerticalFrameSize()) - if docWidth < 84 || docHeight < 24 { - return strings.TrimRight(a.styles.doc.Render(lipgloss.Place(docWidth, docHeight, lipgloss.Left, lipgloss.Top, "Window too small.\nPlease resize to at least 84x24.")), "\n") + if docWidth < 60 || docHeight < 20 { + return strings.TrimRight(a.styles.doc.Render(lipgloss.Place(docWidth, docHeight, lipgloss.Left, lipgloss.Top, "Window too small.\nPlease resize to at least 60x20.")), "\n") } lay := a.computeLayout() @@ -96,37 +90,7 @@ func (a App) renderHeader(width int) string { } func (a App) renderBody(lay layout) string { - sidebar := a.renderSidebar(lay.sidebarWidth, lay.sidebarHeight) - stream := a.renderWaterfall(lay.rightWidth, lay.rightHeight) - if lay.stacked { - return lipgloss.JoinVertical(lipgloss.Left, sidebar, stream) - } - return lipgloss.JoinHorizontal( - lipgloss.Top, - sidebar, - lipgloss.NewStyle().Width(lay.bodyGap).Render(""), - stream, - ) -} - -func (a App) renderSidebar(width int, height int) string { - style := a.styles.panel - if a.focus == panelSessions { - style = a.styles.panelFocused - } - - frameHeight := style.GetVerticalFrameSize() - borderWidth := 2 - paddingWidth := style.GetHorizontalFrameSize() - borderWidth - panelWidth := max(1, width-borderWidth) - bodyWidth := max(10, panelWidth-paddingWidth) - header := a.renderSidebarHeader(bodyWidth) - bodyHeight := max(3, height-frameHeight-lipgloss.Height(header)) - a.sessions.SetSize(bodyWidth, bodyHeight) - body := a.styles.panelBody.Width(bodyWidth).Height(bodyHeight).Render(a.sessions.View()) - - panel := style.Width(panelWidth).Height(max(1, height-frameHeight)).Render(lipgloss.JoinVertical(lipgloss.Left, header, body)) - return lipgloss.Place(width, height, lipgloss.Left, lipgloss.Top, panel) + return a.renderWaterfall(lay.contentWidth, lay.contentHeight) } // waterfallMetrics 统一计算瀑布区各组件高度,确保渲染、布局与命中区域使用同一组尺寸。 @@ -210,23 +174,6 @@ func (a App) renderPrompt(width int) string { return box.Width(boxWidth).Render(a.renderPermissionPrompt()) } -func (a App) renderSidebarHeader(width int) string { - title := a.styles.panelTitle.Render(sidebarTitle) - filterWidth := max(6, width-lipgloss.Width(title)-1) - titleRow := lipgloss.JoinHorizontal( - lipgloss.Center, - title, - lipgloss.NewStyle().Width(1).Render(""), - a.styles.panelSubtitle.Render(tuiutils.TrimRunes(sidebarFilterHint, filterWidth)), - ) - openRow := a.styles.panelSubtitle.Render(tuiutils.TrimRunes(sidebarOpenHint, width)) - return lipgloss.JoinVertical( - lipgloss.Left, - lipgloss.Place(width, 1, lipgloss.Left, lipgloss.Top, titleRow), - lipgloss.Place(width, 1, lipgloss.Left, lipgloss.Top, openRow), - ) -} - func (a App) renderPanel(title string, subtitle string, body string, width int, height int, focused bool) string { style := a.styles.panel if focused { @@ -274,9 +221,7 @@ func (a App) renderMessageBlockWithCopy(message providertypes.Message, width int bodyStyle = a.styles.messageUserBody blockAlign = lipgloss.Right case roleTool: - tag = messageTagTool - tagStyle = a.styles.messageToolTag - bodyStyle = a.styles.messageToolBody + return "", nil } content := strings.TrimSpace(message.Content) @@ -470,22 +415,7 @@ func (a App) computeLayout() layout { helpHeight := a.helpHeight(contentWidth) headerHeight := headerBarHeight contentHeight := max(1, a.height-a.styles.doc.GetVerticalFrameSize()-headerHeight-helpHeight) - lay := layout{contentWidth: contentWidth, contentHeight: contentHeight} - if contentWidth < 110 { - lay.stacked = true - lay.sidebarWidth = contentWidth - lay.sidebarHeight = tuiutils.Clamp(contentHeight/3, 9, 13) - lay.rightWidth = contentWidth - lay.rightHeight = max(10, contentHeight-lay.sidebarHeight) - return lay - } - - lay.bodyGap = 1 - lay.sidebarWidth = 22 - lay.sidebarHeight = contentHeight - lay.rightWidth = max(24, contentWidth-lay.sidebarWidth-lay.bodyGap) - lay.rightHeight = contentHeight - return lay + return layout{contentWidth: contentWidth, contentHeight: contentHeight} } // helpHeight 仅计算帮助区高度,避免在 layout 计算阶段触发完整渲染。 diff --git a/internal/tui/core/app/view_test.go b/internal/tui/core/app/view_test.go index cb638676..dc0562cb 100644 --- a/internal/tui/core/app/view_test.go +++ b/internal/tui/core/app/view_test.go @@ -53,7 +53,7 @@ func TestApplyComponentLayoutKeepsTranscriptHeightInSyncWithWaterfall(t *testing app.applyComponentLayout(false) lay := app.computeLayout() - wantTranscriptHeight, activityHeight, menuHeight, _ := app.waterfallMetrics(app.transcript.Width, lay.rightHeight) + wantTranscriptHeight, activityHeight, menuHeight, _ := app.waterfallMetrics(app.transcript.Width, lay.contentHeight) if app.transcript.Height != wantTranscriptHeight { t.Fatalf("expected transcript height %d, got %d", wantTranscriptHeight, app.transcript.Height) } From 72f5c8ca628a0908b3256677d3bf63d52fdfc445 Mon Sep 17 00:00:00 2001 From: creatang Date: Tue, 14 Apr 2026 08:48:28 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor=EF=BC=88tui=EF=BC=89=EF=BC=9APhase?= =?UTF-8?q?=202:=20Header=20=E6=9E=81=E7=AE=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tui/core/app/styles.go | 16 ---------------- internal/tui/core/app/view.go | 28 +++++----------------------- 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/internal/tui/core/app/styles.go b/internal/tui/core/app/styles.go index 85d6c48c..c8b000c2 100644 --- a/internal/tui/core/app/styles.go +++ b/internal/tui/core/app/styles.go @@ -39,11 +39,6 @@ type styles struct { doc lipgloss.Style headerBar lipgloss.Style headerBrand lipgloss.Style - headerLabel lipgloss.Style - headerPath lipgloss.Style - headerSub lipgloss.Style - headerMeta lipgloss.Style - headerSpacer lipgloss.Style panel lipgloss.Style panelFocused lipgloss.Style panelTitle lipgloss.Style @@ -107,17 +102,6 @@ func newStyles() styles { Foreground(lipgloss.Color(terracotta)). UnsetBackground(). Padding(0, 1), - headerLabel: lipgloss.NewStyle(). - Foreground(lipgloss.Color(oliveGray)), - headerPath: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)), - headerSub: lipgloss.NewStyle(). - Foreground(lipgloss.Color(oliveGray)), - headerMeta: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)), - headerSpacer: lipgloss.NewStyle(). - Width(1). - UnsetBackground(), panel: panel, panelFocused: panel.Copy(). BorderForeground(lipgloss.Color(terracotta)), diff --git a/internal/tui/core/app/view.go b/internal/tui/core/app/view.go index 0833c1bc..11362c71 100644 --- a/internal/tui/core/app/view.go +++ b/internal/tui/core/app/view.go @@ -43,7 +43,7 @@ func (a App) View() string { } func (a App) renderHeader(width int) string { - status := tuicomponents.CompactStatusText(a.state.StatusText, max(18, width/3)) + status := a.state.StatusText if a.state.IsAgentRunning { if a.runProgressKnown { progressBar := a.progress @@ -55,36 +55,18 @@ func (a App) renderHeader(width int) string { } } - brand := lipgloss.JoinHorizontal( - lipgloss.Center, - a.styles.headerBrand.Render("NeoCode"), - ) + brand := a.styles.headerBrand.Render("NeoCode") - meta := lipgloss.JoinHorizontal( + modelStatus := lipgloss.JoinHorizontal( lipgloss.Top, - a.styles.badgeAgent.Render(a.state.CurrentProvider), a.styles.badgeUser.Render(a.state.CurrentModel), - a.styles.badgeMuted.Render(a.focusLabel()), a.statusBadge(status), ) - spacerWidth := lipgloss.Width(a.styles.headerSpacer.Render("")) - workdirLabel := a.styles.headerLabel.Render("Workdir") - workdirWidth := max(12, width-lipgloss.Width(brand)-lipgloss.Width(meta)-(spacerWidth*2)-lipgloss.Width(workdirLabel)-1) - workdir := lipgloss.JoinHorizontal( - lipgloss.Center, - workdirLabel, - " ", - a.styles.headerPath.Render(tuiutils.TrimMiddle(a.state.CurrentWorkdir, workdirWidth)), - ) - header := lipgloss.JoinHorizontal( - lipgloss.Center, + lipgloss.Top, brand, - a.styles.headerSpacer.Render(""), - workdir, - a.styles.headerSpacer.Render(""), - meta, + modelStatus, ) return a.styles.headerBar.Width(width).Render(lipgloss.Place(width, 1, lipgloss.Left, lipgloss.Top, header)) } From 5dffdc171f2b0ec80f5d9eae9e2f91150e293922 Mon Sep 17 00:00:00 2001 From: creatang Date: Tue, 14 Apr 2026 09:17:54 +0800 Subject: [PATCH 3/5] =?UTF-8?q?refactor=EF=BC=88tui=EF=BC=89=EF=BC=9APhase?= =?UTF-8?q?=203:=20=E8=BE=93=E5=85=A5=E6=A1=86=E5=BA=95=E9=83=A8=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tui/core/app/styles.go | 3 ++- internal/tui/core/app/update.go | 4 ++-- internal/tui/core/app/view.go | 21 +++++++++------------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/internal/tui/core/app/styles.go b/internal/tui/core/app/styles.go index c8b000c2..1949ec7a 100644 --- a/internal/tui/core/app/styles.go +++ b/internal/tui/core/app/styles.go @@ -96,7 +96,8 @@ func newStyles() styles { Foreground(lipgloss.Color(anthropicNearBlack)), headerBar: lipgloss.NewStyle(). UnsetBackground(). - Foreground(lipgloss.Color(anthropicNearBlack)), + Foreground(lipgloss.Color(anthropicNearBlack)). + Underline(true), headerBrand: lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color(terracotta)). diff --git a/internal/tui/core/app/update.go b/internal/tui/core/app/update.go index 75fe869d..a5515968 100644 --- a/internal/tui/core/app/update.go +++ b/internal/tui/core/app/update.go @@ -1499,11 +1499,11 @@ func (a *App) applyComponentLayout(rebuildTranscript bool) { } func (a App) composerBoxWidth(totalWidth int) int { - return max(8, totalWidth-2) + return totalWidth } func (a App) composerInnerWidth(totalWidth int) int { - return max(4, a.composerBoxWidth(totalWidth)-a.styles.inputBoxFocused.GetHorizontalFrameSize()) + return max(4, totalWidth-a.styles.inputBoxFocused.GetHorizontalFrameSize()) } func (a App) composerHeight() int { diff --git a/internal/tui/core/app/view.go b/internal/tui/core/app/view.go index 11362c71..ed366bb7 100644 --- a/internal/tui/core/app/view.go +++ b/internal/tui/core/app/view.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/lipgloss" providertypes "neo-code/internal/provider/types" @@ -68,7 +67,7 @@ func (a App) renderHeader(width int) string { brand, modelStatus, ) - return a.styles.headerBar.Width(width).Render(lipgloss.Place(width, 1, lipgloss.Left, lipgloss.Top, header)) + return a.styles.headerBar.Width(width).Height(2).Render(header) } func (a App) renderBody(lay layout) string { @@ -79,9 +78,8 @@ func (a App) renderBody(lay layout) string { func (a App) waterfallMetrics(width int, height int) (int, int, int, int) { activityHeight := a.activityPreviewHeight() menuHeight := a.commandMenuHeight(width) - promptHeight := lipgloss.Height(a.renderPrompt(width)) - transcriptHeight := max(6, height-activityHeight-menuHeight-promptHeight) - return transcriptHeight, activityHeight, menuHeight, promptHeight + transcriptHeight := max(6, height-activityHeight-menuHeight) + return transcriptHeight, activityHeight, menuHeight, 0 } func (a App) renderWaterfall(width int, height int) string { @@ -146,14 +144,17 @@ func (a App) renderPicker(width int, height int) string { } func (a App) renderPrompt(width int) string { + if a.pendingPermission != nil { + box := a.styles.inputBoxFocused + return box.Width(width).Margin(1, 0, 0, 0).Render(a.renderPermissionPrompt()) + } + box := a.styles.inputBox if a.focus == panelInput && a.state.ActivePicker == pickerNone { box = a.styles.inputBoxFocused } - // Account for frame and padding when sizing the composer container. - boxWidth := a.composerBoxWidth(width) - return box.Width(boxWidth).Render(a.renderPermissionPrompt()) + return box.Width(width).Margin(1, 0, 0, 0).Render(a.input.View()) } func (a App) renderPanel(title string, subtitle string, body string, width int, height int, focused bool) string { @@ -405,7 +406,3 @@ func (a App) helpHeight(width int) int { a.help.ShowAll = a.state.ShowHelp return lipgloss.Height(a.styles.footer.Width(width).Render(a.help.View(a.keys))) } - -func (a App) isFilteringSessions() bool { - return a.sessions.FilterState() != list.Unfiltered -} From 04025e75a87914cc6795f17877a8dd3ec5ac1c6e Mon Sep 17 00:00:00 2001 From: creatang Date: Tue, 14 Apr 2026 09:29:02 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor=EF=BC=88tui=EF=BC=89=EF=BC=9APhase?= =?UTF-8?q?=204:=20=E6=B6=88=E6=81=AF=E6=A0=B7=E5=BC=8F=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/tui/core/app/styles.go | 22 +++++----------------- internal/tui/core/app/view.go | 4 +--- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/internal/tui/core/app/styles.go b/internal/tui/core/app/styles.go index 1949ec7a..ee8af973 100644 --- a/internal/tui/core/app/styles.go +++ b/internal/tui/core/app/styles.go @@ -149,35 +149,23 @@ func newStyles() styles { messageBody: lipgloss.NewStyle(). Foreground(lipgloss.Color(anthropicNearBlack)). UnsetBackground(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(borderWarm)). - Padding(0, 0), + MarginLeft(2), messageUserBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)). + Foreground(lipgloss.Color(terracotta)). UnsetBackground(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(terracotta)). - Padding(0, 0), + Bold(true). + MarginRight(2), inlineNotice: lipgloss.NewStyle(). Foreground(lipgloss.Color(oliveGray)). UnsetBackground(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(borderWarm)). - Padding(0, 0). Italic(true), inlineError: lipgloss.NewStyle(). Foreground(lipgloss.Color(errorCrimson)). UnsetBackground(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(errorCrimson)). - Padding(0, 0). Bold(true), inlineSystem: lipgloss.NewStyle(). Foreground(lipgloss.Color(oliveGray)). - UnsetBackground(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(borderWarm)). - Padding(0, 0), + UnsetBackground(), codeBlock: lipgloss.NewStyle(). MarginLeft(1). Padding(0, 0). diff --git a/internal/tui/core/app/view.go b/internal/tui/core/app/view.go index ed366bb7..d3a39a51 100644 --- a/internal/tui/core/app/view.go +++ b/internal/tui/core/app/view.go @@ -194,7 +194,6 @@ func (a App) renderMessageBlockWithCopy(message providertypes.Message, width int tag := messageTagAgent tagStyle := a.styles.messageAgentTag bodyStyle := a.styles.messageBody - blockAlign := lipgloss.Left switch message.Role { case roleUser: @@ -202,7 +201,6 @@ func (a App) renderMessageBlockWithCopy(message providertypes.Message, width int tag = messageTagUser tagStyle = a.styles.messageUserTag bodyStyle = a.styles.messageUserBody - blockAlign = lipgloss.Right case roleTool: return "", nil } @@ -229,7 +227,7 @@ func (a App) renderMessageBlockWithCopy(message providertypes.Message, width int contentBlock, copyButtons = a.renderMessageContentWithCopy(content, maxMessageWidth-2, bodyStyle, startCopyID) } parts := []string{tagStyle.Render(tag), contentBlock} - block := lipgloss.JoinVertical(blockAlign, parts...) + block := lipgloss.JoinVertical(lipgloss.Left, parts...) if message.Role == roleUser { return lipgloss.PlaceHorizontal(width, lipgloss.Right, block), nil From 5cea84f8fba902a0a02d6f5dedbce7739febf744 Mon Sep 17 00:00:00 2001 From: creatang Date: Tue, 14 Apr 2026 15:07:09 +0800 Subject: [PATCH 5/5] fix(tui): stabilize header rendering and remove ANSI artifacts --- internal/tui/core/app/app.go | 12 +-- internal/tui/core/app/styles.go | 138 ++++++++++++----------------- internal/tui/core/app/view.go | 56 +++++++----- internal/tui/core/app/view_test.go | 55 ++++++++++++ 4 files changed, 153 insertions(+), 108 deletions(-) diff --git a/internal/tui/core/app/app.go b/internal/tui/core/app/app.go index 4885ac67..a42261cb 100644 --- a/internal/tui/core/app/app.go +++ b/internal/tui/core/app/app.go @@ -181,24 +181,24 @@ func newApp(container tuibootstrap.Container) (App, error) { return "> " }) input.FocusedStyle.Base = lipgloss.NewStyle() - input.FocusedStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)).Bold(true) - input.FocusedStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)) + input.FocusedStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(lightText)).Bold(true) + input.FocusedStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(lightText)) input.FocusedStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(oliveGray)) input.FocusedStyle.CursorLine = lipgloss.NewStyle() input.FocusedStyle.CursorLineNumber = lipgloss.NewStyle() input.BlurredStyle.Base = lipgloss.NewStyle() - input.BlurredStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)).Bold(true) - input.BlurredStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(anthropicNearBlack)) + input.BlurredStyle.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color(lightText)).Bold(true) + input.BlurredStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color(lightText)) input.BlurredStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color(oliveGray)) input.BlurredStyle.CursorLine = lipgloss.NewStyle() input.BlurredStyle.CursorLineNumber = lipgloss.NewStyle() - input.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(terracotta)) + input.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(purpleAccent)) input.SetHeight(composerMinHeight) input.Focus() spin := spinner.New() spin.Spinner = spinner.Line - spin.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(terracotta)) + spin.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(purpleAccent)) h := help.New() h.ShowAll = false diff --git a/internal/tui/core/app/styles.go b/internal/tui/core/app/styles.go index ee8af973..9a48c67e 100644 --- a/internal/tui/core/app/styles.go +++ b/internal/tui/core/app/styles.go @@ -7,32 +7,29 @@ import ( ) const ( - parchment = "#f5f4ed" - ivory = "#faf9f5" - pureWhite = "#ffffff" - warmSand = "#e8e6dc" - darkSurface = "#30302e" - deepDark = "#141413" + purpleBg = "#1a1625" + purpleBg2 = "#251f35" + purpleSurface = "#352d47" - anthropicNearBlack = "#141413" - terracotta = "#c96442" - coralAccent = "#d97757" + lightText = "#e8e6f0" + lightText2 = "#c8c6d8" + midGray = "#7a7890" - errorCrimson = "#b53333" - focusBlue = "#3898ec" + purpleAccent = "#a78bfa" + purpleLight = "#c4b5fd" + coralAccent = "#f09070" - charcoalWarm = "#4d4c48" - oliveGray = "#5e5d59" - stoneGray = "#87867f" - darkWarm = "#3d3d3a" - warmSilver = "#b0aea5" + errorRed = "#f87171" + successGreen = "#34d399" + warningYellow = "#fbbf24" - borderCream = "#f0eee6" - borderWarm = "#e8e6dc" - borderDark = "#30302e" - ringWarm = "#d1cfc5" - ringSubtle = "#dedc01" - ringDeep = "#c2c0b6" + charcoal = "#4a4560" + oliveGray = "#6b6588" + stoneGray = "#9089a8" + warmSilver = "#a9a4b8" + + borderDark = "#3d3654" + borderLight = "#4a4268" ) type styles struct { @@ -83,96 +80,85 @@ type styles struct { } func newStyles() styles { + subtleText := lipgloss.AdaptiveColor{Light: oliveGray, Dark: lightText2} + headerAccent := lipgloss.AdaptiveColor{Light: coralAccent, Dark: purpleLight} + panel := lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(borderWarm)). - UnsetBackground(). + BorderForeground(lipgloss.Color(borderDark)). Padding(0, 1) return styles{ doc: lipgloss.NewStyle(). Padding(1, 2, 0, 2). - UnsetBackground(). - Foreground(lipgloss.Color(anthropicNearBlack)), + UnsetBackground(), headerBar: lipgloss.NewStyle(). - UnsetBackground(). - Foreground(lipgloss.Color(anthropicNearBlack)). - Underline(true), + UnsetBackground(), headerBrand: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(terracotta)). + Foreground(headerAccent). UnsetBackground(). Padding(0, 1), panel: panel, panelFocused: panel.Copy(). - BorderForeground(lipgloss.Color(terracotta)), + BorderForeground(lipgloss.Color(purpleAccent)), panelTitle: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(anthropicNearBlack)), + Foreground(lipgloss.Color(lightText)), panelSubtitle: lipgloss.NewStyle(). Foreground(lipgloss.Color(oliveGray)), panelBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)). - UnsetBackground(), + Foreground(lipgloss.Color(lightText)), empty: lipgloss.NewStyle(). - Foreground(lipgloss.Color(stoneGray)). + Foreground(lipgloss.Color(midGray)). Padding(1, 0), sessionRow: lipgloss.NewStyle(). Padding(0, 1). - Foreground(lipgloss.Color(anthropicNearBlack)). - UnsetBackground(), + Foreground(lipgloss.Color(lightText)), sessionRowActive: lipgloss.NewStyle(). Padding(0, 1). - Foreground(lipgloss.Color(anthropicNearBlack)). - UnsetBackground(), + Foreground(lipgloss.Color(lightText)), sessionRowFocused: lipgloss.NewStyle(). Padding(0, 1). - Foreground(lipgloss.Color(anthropicNearBlack)). - UnsetBackground(). + Foreground(lipgloss.Color(lightText)). Bold(true), sessionMeta: lipgloss.NewStyle(). Foreground(lipgloss.Color(oliveGray)), sessionMetaActive: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)), + Foreground(lipgloss.Color(lightText)), sessionMetaFocus: lipgloss.NewStyle(). - Foreground(lipgloss.Color(terracotta)), + Foreground(lipgloss.Color(purpleAccent)), streamTitle: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(anthropicNearBlack)), + Foreground(lipgloss.Color(lightText)), streamMeta: lipgloss.NewStyle(). Foreground(lipgloss.Color(oliveGray)), streamContent: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)). - UnsetBackground(), - messageUserTag: tagStyle(charcoalWarm), - messageAgentTag: tagStyle(terracotta), + Foreground(lipgloss.Color(lightText)), + messageUserTag: tagStyle(purpleAccent), + messageAgentTag: tagStyle(purpleLight), messageBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)). - UnsetBackground(). + Foreground(lipgloss.Color(lightText2)). MarginLeft(2), messageUserBody: lipgloss.NewStyle(). - Foreground(lipgloss.Color(terracotta)). - UnsetBackground(). + Foreground(lipgloss.Color(purpleAccent)). Bold(true). - MarginRight(2), + MarginRight(8), inlineNotice: lipgloss.NewStyle(). Foreground(lipgloss.Color(oliveGray)). - UnsetBackground(). Italic(true), inlineError: lipgloss.NewStyle(). - Foreground(lipgloss.Color(errorCrimson)). - UnsetBackground(). + Foreground(lipgloss.Color(errorRed)). Bold(true), inlineSystem: lipgloss.NewStyle(). - Foreground(lipgloss.Color(oliveGray)). - UnsetBackground(), + Foreground(lipgloss.Color(oliveGray)), codeBlock: lipgloss.NewStyle(). MarginLeft(1). - Padding(0, 0). + Padding(1, 0). UnsetBackground(). Border(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(borderDark)), + BorderForeground(lipgloss.Color(borderLight)), codeText: lipgloss.NewStyle(). Foreground(lipgloss.Color(warmSilver)), codeCopyButton: lipgloss.NewStyle(). @@ -180,41 +166,37 @@ func newStyles() styles { Foreground(lipgloss.Color(coralAccent)). Underline(true), commandMenu: lipgloss.NewStyle(). - UnsetBackground(). - Padding(1, 1), + MarginTop(1), commandMenuTitle: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(terracotta)), + Foreground(lipgloss.Color(purpleAccent)), commandUsage: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)), + Foreground(lipgloss.Color(lightText)), commandUsageMatch: lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color(terracotta)), + Foreground(lipgloss.Color(purpleAccent)), commandDesc: lipgloss.NewStyle(). Foreground(lipgloss.Color(oliveGray)), inputPrefix: lipgloss.NewStyle(). - Foreground(lipgloss.Color(terracotta)). + Foreground(lipgloss.Color(purpleAccent)). Bold(true), inputLine: lipgloss.NewStyle(). - Foreground(lipgloss.Color(anthropicNearBlack)), + Foreground(lipgloss.Color(lightText)), inputBox: lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(borderWarm)). - UnsetBackground(). + BorderForeground(lipgloss.Color(borderDark)). Padding(0, 1), inputBoxFocused: lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(focusBlue)). - UnsetBackground(). + BorderForeground(lipgloss.Color(purpleAccent)). Padding(0, 1), footer: lipgloss.NewStyle(). - Foreground(lipgloss.Color(oliveGray)). - UnsetBackground(), - badgeUser: badge("", charcoalWarm), + Foreground(subtleText), + badgeUser: badge("", purpleAccent), badgeAgent: badge("", coralAccent), - badgeSuccess: badge("", oliveGray), - badgeWarning: badge("", charcoalWarm), - badgeError: badge("", errorCrimson), + badgeSuccess: badge("", successGreen), + badgeWarning: badge("", warningYellow), + badgeError: badge("", errorRed), badgeMuted: badge("", stoneGray), } } @@ -223,7 +205,6 @@ func tagStyle(fg string) lipgloss.Style { return lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color(fg)). - UnsetBackground(). Padding(0, 1) } @@ -231,7 +212,6 @@ func badge(bg string, fg string) lipgloss.Style { return lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color(fg)). - UnsetBackground(). Padding(0, 1) } diff --git a/internal/tui/core/app/view.go b/internal/tui/core/app/view.go index d3a39a51..355dd223 100644 --- a/internal/tui/core/app/view.go +++ b/internal/tui/core/app/view.go @@ -17,7 +17,7 @@ type layout struct { contentHeight int } -const headerBarHeight = 1 +const headerBarHeight = 2 func (a App) View() string { docWidth := max(0, a.width-a.styles.doc.GetHorizontalFrameSize()) @@ -42,32 +42,24 @@ func (a App) View() string { } func (a App) renderHeader(width int) string { - status := a.state.StatusText + status := compactStatusText(a.state.StatusText, max(18, width/3)) if a.state.IsAgentRunning { if a.runProgressKnown { - progressBar := a.progress - progressBar.Width = tuiutils.Clamp(width/7, 12, 26) progressLabel := tuiutils.Fallback(strings.TrimSpace(a.runProgressLabel), tuiutils.Fallback(status, statusRunning)) - status = progressBar.ViewAs(a.runProgressValue) + " " + progressLabel - } else { - status = a.spinner.View() + " " + tuiutils.Fallback(status, statusRunning) + percent := int(a.runProgressValue*100 + 0.5) + status = fmt.Sprintf("%d%% %s", percent, progressLabel) + } else if status != statusThinking { + status = tuiutils.Fallback(status, statusRunning) } } + status = tuiutils.Fallback(status, statusReady) - brand := a.styles.headerBrand.Render("NeoCode") - - modelStatus := lipgloss.JoinHorizontal( - lipgloss.Top, - a.styles.badgeUser.Render(a.state.CurrentModel), - a.statusBadge(status), - ) - - header := lipgloss.JoinHorizontal( - lipgloss.Top, - brand, - modelStatus, - ) - return a.styles.headerBar.Width(width).Height(2).Render(header) + model := tuiutils.Fallback(strings.TrimSpace(a.state.CurrentModel), "unknown-model") + headerText := fmt.Sprintf("NeoCode | %s | %s", model, status) + if lipgloss.Width(headerText) > width { + headerText = tuiutils.TrimMiddle(headerText, max(8, width)) + } + return a.styles.headerBar.Width(width).Height(headerBarHeight).Render(headerText) } func (a App) renderBody(lay layout) string { @@ -98,6 +90,12 @@ func (a App) renderWaterfall(width int, height int) string { transcript := a.styles.streamContent.Width(width).Height(transcriptHeight).Render(a.transcript.View()) parts := []string{transcript} + if a.state.IsAgentRunning && a.state.StatusText == statusThinking { + thinkingStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color(oliveGray)). + Italic(true) + parts = append(parts, thinkingStyle.Render("Thinking...")) + } if activity := a.renderActivityPreview(width); activity != "" { parts = append(parts, activity) } @@ -226,8 +224,20 @@ func (a App) renderMessageBlockWithCopy(message providertypes.Message, width int } else { contentBlock, copyButtons = a.renderMessageContentWithCopy(content, maxMessageWidth-2, bodyStyle, startCopyID) } - parts := []string{tagStyle.Render(tag), contentBlock} - block := lipgloss.JoinVertical(lipgloss.Left, parts...) + tagLine := tagStyle.Render(tag) + blockAlign := lipgloss.Left + if message.Role == roleUser { + blockAlign = lipgloss.Right + rightInset := bodyStyle.GetMarginRight() - tagStyle.GetPaddingRight() + if rightInset < 0 { + rightInset = 0 + } + if rightInset > 0 { + tagLine = tagStyle.Copy().MarginRight(rightInset).Render(tag) + } + } + parts := []string{tagLine, contentBlock} + block := lipgloss.JoinVertical(blockAlign, parts...) if message.Role == roleUser { return lipgloss.PlaceHorizontal(width, lipgloss.Right, block), nil diff --git a/internal/tui/core/app/view_test.go b/internal/tui/core/app/view_test.go index dc0562cb..edd44261 100644 --- a/internal/tui/core/app/view_test.go +++ b/internal/tui/core/app/view_test.go @@ -5,7 +5,9 @@ import ( "testing" "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/lipgloss" + providertypes "neo-code/internal/provider/types" tuistate "neo-code/internal/tui/state" ) @@ -74,3 +76,56 @@ func TestApplyComponentLayoutKeepsTranscriptHeightInSyncWithWaterfall(t *testing t.Fatalf("expected input Y %d, got %d", transcriptY+wantTranscriptHeight+activityHeight+menuHeight, inputY) } } + +func TestComputeLayoutUsesRenderedHeaderHeight(t *testing.T) { + app, _ := newTestApp(t) + app.width = 100 + app.height = 30 + + lay := app.computeLayout() + header := app.renderHeader(lay.contentWidth) + if got := lipgloss.Height(header); got != headerBarHeight { + t.Fatalf("expected header height %d, got %d", headerBarHeight, got) + } + if strings.Contains(header, "\x1b[") { + t.Fatalf("expected header to avoid ANSI escapes, got %q", header) + } +} + +func TestRenderUserMessageKeepsTagAndBodyRightAligned(t *testing.T) { + app, _ := newTestApp(t) + + block, _ := app.renderMessageBlockWithCopy(providertypes.Message{ + Role: roleUser, + Content: "hello right aligned", + }, 72, 1) + + plain := copyCodeANSIPattern.ReplaceAllString(block, "") + lines := strings.Split(plain, "\n") + + var ( + tagLine string + contentLine string + ) + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + if strings.Contains(line, messageTagUser) { + tagLine = line + } + if strings.Contains(line, "hello right aligned") { + contentLine = line + } + } + if tagLine == "" || contentLine == "" { + t.Fatalf("expected user tag and content lines, got %q", plain) + } + + tagRightEdge := lipgloss.Width(strings.TrimRight(tagLine, " ")) + bodyRightEdge := lipgloss.Width(strings.TrimRight(contentLine, " ")) + if tagRightEdge != bodyRightEdge { + t.Fatalf("expected user tag and body right edges to match, got tag=%d body=%d\n%q\n%q", tagRightEdge, bodyRightEdge, tagLine, contentLine) + } +}