Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
Expand All @@ -28,8 +28,10 @@ type App struct {
providerPicker list.Model
modelPicker list.Model
transcript viewport.Model
input textinput.Model
input textarea.Model
activeMessages []provider.Message
codeBlocks []codeBlockTarget
transcriptRect rect
focus panel
width int
height int
Expand Down Expand Up @@ -63,13 +65,18 @@ func New(cfg *config.Config, configManager *config.Manager, runtime agentruntime
sessionList.FilterInput.Prompt = "Filter: "
sessionList.FilterInput.Placeholder = "Type to search sessions"

input := textinput.New()
input := textarea.New()
input.Placeholder = "Ask NeoCode to inspect, edit, or build. Type / to browse commands."
input.Prompt = ""
input.Prompt = "> "
input.CharLimit = 24000
input.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorUser))
input.TextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorText))
input.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorSubtle))
input.ShowLineNumbers = false
input.EndOfBufferCharacter = ' '
input.SetHeight(1)
input.FocusedStyle.Base = lipgloss.NewStyle().Foreground(lipgloss.Color(colorText))
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(colorUser)).Bold(true)
input.BlurredStyle = input.FocusedStyle
input.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(colorUser))
input.Focus()

Expand Down Expand Up @@ -130,5 +137,5 @@ func New(cfg *config.Config, configManager *config.Manager, runtime agentruntime
}

func (a App) Init() tea.Cmd {
return tea.Batch(ListenForRuntimeEvent(a.runtime.Events()), textinput.Blink, a.spinner.Tick)
return tea.Batch(ListenForRuntimeEvent(a.runtime.Events()), textarea.Blink, a.spinner.Tick)
}
117 changes: 117 additions & 0 deletions internal/tui/interaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package tui

import (
"fmt"
"strings"

"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

var writeClipboard = clipboard.WriteAll

func (a *App) shouldSendInput(msg tea.KeyMsg) bool {
return key.Matches(msg, a.keys.Send)
}

func (a *App) handleMouse(msg tea.MouseMsg) {
if !a.transcriptRect.contains(msg.X, msg.Y) {
return
}

switch msg.Button { //nolint:exhaustive
case tea.MouseButtonWheelUp:
if msg.Action == tea.MouseActionPress {
a.focus = panelTranscript
a.applyFocus()
a.transcript.LineUp(3)
}
case tea.MouseButtonWheelDown:
if msg.Action == tea.MouseActionPress {
a.focus = panelTranscript
a.applyFocus()
a.transcript.LineDown(3)
}
case tea.MouseButtonLeft:
if msg.Action != tea.MouseActionPress {
return
}
if target := a.hitCodeBlockTarget(msg.X, msg.Y); target != nil {
a.copyCodeBlock(*target)
return
}
a.focus = panelTranscript
a.applyFocus()
}
}

func (a *App) hitCodeBlockTarget(mouseX int, mouseY int) *codeBlockTarget {
if !a.transcriptRect.contains(mouseX, mouseY) {
return nil
}

contentLine := a.transcript.YOffset + (mouseY - a.transcriptRect.Y)
contentX := mouseX - a.transcriptRect.X
for i := range a.codeBlocks {
target := &a.codeBlocks[i]
if target.Line != contentLine {
continue
}
if contentX >= target.X && contentX < target.X+target.Width {
return target
}
}
return nil
}

func (a *App) copyCodeBlock(target codeBlockTarget) {
if err := writeClipboard(target.Content); err != nil {
notice := fmt.Sprintf("Copy failed: %v", err)
a.state.ExecutionError = notice
a.state.StatusText = notice
a.appendInlineMessage(roleError, notice)
a.rebuildTranscript()
return
}

label := "code block"
if trimmed := strings.TrimSpace(target.Language); trimmed != "" && !strings.EqualFold(trimmed, "text") {
label = trimmed + " code block"
}
notice := fmt.Sprintf("Copied %s.", label)
a.state.ExecutionError = ""
a.state.StatusText = notice
a.appendInlineMessage(roleSystem, notice)
a.rebuildTranscript()
}

func (a *App) composerRows(availableWidth int) int {
width := max(8, availableWidth-lipgloss.Width(a.input.Prompt))
lines := wrappedLineCount(a.input.Value(), width)
if strings.TrimSpace(a.input.Value()) == "" {
lines = max(lines, 1)
}
return clamp(lines, 1, 8)
}

func (a *App) updateTranscriptRect(lay layout) {
docX := a.styles.doc.GetPaddingLeft()
docY := a.styles.doc.GetPaddingTop()
headerHeight := lipgloss.Height(a.renderHeader(lay.contentWidth))
rightX := docX
rightY := docY + headerHeight
if lay.stacked {
rightY += lay.sidebarHeight
} else {
rightX += lay.sidebarWidth + lay.bodyGap
}

a.transcriptRect = rect{
X: rightX,
Y: rightY,
Width: a.transcript.Width,
Height: a.transcript.Height,
}
}
32 changes: 16 additions & 16 deletions internal/tui/keymap.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,64 +23,64 @@ type keyMap struct {
func newKeyMap() keyMap {
return keyMap{
Send: key.NewBinding(
key.WithKeys("enter", "ctrl+s"),
key.WithHelp("Enter/Ctrl+S", "发送(输入框)"),
key.WithKeys("ctrl+j"),
key.WithHelp("Ctrl+J", "send"),
),
CancelAgent: key.NewBinding(
key.WithKeys("ctrl+w"),
key.WithHelp("Ctrl+w", "中止"),
key.WithHelp("Ctrl+W", "cancel"),
),
NewSession: key.NewBinding(
key.WithKeys("ctrl+n"),
key.WithHelp("Ctrl+N", "新会话"),
key.WithHelp("Ctrl+N", "new"),
),
NextPanel: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("Tab", "下个面板"),
key.WithHelp("Tab", "next panel"),
),
PrevPanel: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("Shift+Tab", "上个面板"),
key.WithHelp("Shift+Tab", "prev panel"),
),
FocusInput: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("Esc", "聚焦输入框"),
key.WithHelp("Esc", "focus input"),
),
OpenSession: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("Enter", "打开会话"),
key.WithHelp("Enter", "open session"),
),
ToggleHelp: key.NewBinding(
key.WithKeys("ctrl+q"),
key.WithHelp("Ctrl+Q", "帮助"),
key.WithHelp("Ctrl+Q", "help"),
),
Quit: key.NewBinding(
key.WithKeys("ctrl+u"),
key.WithHelp("Ctrl+U", "退出"),
key.WithHelp("Ctrl+U", "quit"),
),
ScrollUp: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("Up/k", "向上滚动"),
key.WithHelp("Up/k", "scroll up"),
),
ScrollDown: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("Down/j", "向下滚动"),
key.WithHelp("Down/j", "scroll down"),
),
PageUp: key.NewBinding(
key.WithKeys("pgup", "b"),
key.WithHelp("PgUp/b", "向上翻页"),
key.WithHelp("PgUp/b", "page up"),
),
PageDown: key.NewBinding(
key.WithKeys("pgdown", "f"),
key.WithHelp("PgDn/f", "向下翻页"),
key.WithHelp("PgDn/f", "page down"),
),
Top: key.NewBinding(
key.WithKeys("g", "home"),
key.WithHelp("g/Home", "跳到顶部"),
key.WithHelp("g/Home", "top"),
),
Bottom: key.NewBinding(
key.WithKeys("G", "end"),
key.WithHelp("G/End", "跳到底部"),
key.WithHelp("G/End", "bottom"),
),
}
}
Expand Down
21 changes: 21 additions & 0 deletions internal/tui/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ type activityEntry struct {
IsError bool
}

type rect struct {
X int
Y int
Width int
Height int
}

func (r rect) contains(x int, y int) bool {
return x >= r.X && x < r.X+r.Width && y >= r.Y && y < r.Y+r.Height
}

type codeBlockTarget struct {
MessageIndex int
BlockIndex int
Language string
Content string
Line int
X int
Width int
}

type sessionItem struct {
Summary agentruntime.SessionSummary
Active bool
Expand Down
13 changes: 12 additions & 1 deletion internal/tui/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ type styles struct {
inlineNotice lipgloss.Style
inlineError lipgloss.Style
inlineSystem lipgloss.Style
codeHeader lipgloss.Style
codeLanguage lipgloss.Style
codeCopy lipgloss.Style
codeBlock lipgloss.Style
codeText lipgloss.Style
commandMenu lipgloss.Style
Expand Down Expand Up @@ -176,8 +179,16 @@ func newStyles() styles {
Foreground(lipgloss.Color(colorSubtle)).
Background(lipgloss.Color(colorPanel)).
Padding(0, 1),
codeHeader: lipgloss.NewStyle().
Foreground(lipgloss.Color(colorText)).
Background(lipgloss.Color(colorPanelAlt)),
codeLanguage: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorPrimary)),
codeCopy: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(colorSuccess)),
codeBlock: lipgloss.NewStyle().
MarginLeft(1).
Padding(0, 1).
Background(lipgloss.Color(colorCode)).
BorderLeft(true).
Expand Down
Loading