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
4 changes: 4 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,10 @@ export namespace Config {
.string()
.optional()
.describe("Custom username to display in conversations instead of system username"),
keybinding_mode: z
.enum(["vim"])
.optional()
.describe("Keybinding mode for the TUI input experience (for example: \"vim\")"),
mode: z
.object({
build: Agent.optional(),
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/go/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ type Config struct {
Username string `json:"username"`
Watcher ConfigWatcher `json:"watcher"`
JSON configJSON `json:"-"`
// Keybinding mode for the TUI input experience (for example: "vim")
KeybindingMode string `json:"keybinding_mode"`
}

// configJSON contains the JSON metadata for the struct [Config]
Expand Down Expand Up @@ -123,6 +125,7 @@ type configJSON struct {
Tui apijson.Field
Username apijson.Field
Watcher apijson.Field
KeybindingMode apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
Expand Down
134 changes: 134 additions & 0 deletions packages/tui/internal/components/chat/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ type EditorComponent interface {
SetInterruptKeyInDebounce(inDebounce bool)
SetExitKeyInDebounce(inDebounce bool)
RestoreFromHistory(index int)
MoveLeft()
MoveRight()
MoveUp()
MoveDown()
MoveWordForward()
MoveWordBackward()
MoveWordEnd()
MoveLineStart()
MoveFirstNonBlank()
MoveLineEnd()
DeleteUnderCursor() bool
ReplaceUnderCursor(r rune) bool
BeginSelection()
ClearSelection()
SelectionActive() bool
SelectionText() string
DeleteSelection() string
InsertLineBelow(copyIndent bool)
InsertLineAbove(copyIndent bool)
DeleteCurrentLine() string
BeginLineSelection()
DeleteWordBackward()
DeleteWordForward()
DeleteToLineEnd()
DeleteToLineStart()
IsCursorAtEnd() bool
}

type editorComponent struct {
Expand Down Expand Up @@ -741,6 +767,10 @@ func updateTextareaStyles(ta textarea.Model) textarea.Model {
Foreground(t.Text()).
Background(t.Secondary()).
Lipgloss()
ta.Styles.Selection = styles.NewStyle().
Foreground(t.Background()).
Background(t.Primary()).
Lipgloss()
ta.Styles.Cursor.Color = t.Primary()
return ta
}
Expand Down Expand Up @@ -814,6 +844,110 @@ func (m *editorComponent) RestoreFromHistory(index int) {
m.RestoreFromPrompt(entry)
}

func (m *editorComponent) MoveLeft() {
m.textarea.MoveLeft()
}

func (m *editorComponent) MoveRight() {
m.textarea.MoveRight()
}

func (m *editorComponent) MoveUp() {
m.textarea.MoveUp()
}

func (m *editorComponent) MoveDown() {
m.textarea.MoveDown()
}

func (m *editorComponent) MoveWordForward() {
m.textarea.MoveWordForward()
}

func (m *editorComponent) MoveWordBackward() {
m.textarea.MoveWordBackward()
}

func (m *editorComponent) MoveWordEnd() {
m.textarea.MoveWordEnd()
}

func (m *editorComponent) MoveLineStart() {
m.textarea.MoveLineStart()
}

func (m *editorComponent) MoveFirstNonBlank() {
m.textarea.MoveFirstNonBlank()
}

func (m *editorComponent) MoveLineEnd() {
m.textarea.MoveLineEnd()
}

func (m *editorComponent) DeleteUnderCursor() bool {
return m.textarea.DeleteUnderCursor()
}

func (m *editorComponent) ReplaceUnderCursor(r rune) bool {
return m.textarea.ReplaceUnderCursor(r)
}

func (m *editorComponent) BeginSelection() {
m.textarea.BeginSelection()
}

func (m *editorComponent) ClearSelection() {
m.textarea.ClearSelection()
}

func (m *editorComponent) SelectionActive() bool {
return m.textarea.SelectionActive()
}

func (m *editorComponent) SelectionText() string {
return m.textarea.SelectionText()
}

func (m *editorComponent) DeleteSelection() string {
return m.textarea.DeleteSelection()
}

func (m *editorComponent) DeleteWordBackward() {
m.textarea.DeleteWordBackward()
}

func (m *editorComponent) DeleteWordForward() {
m.textarea.DeleteWordForward()
}

func (m *editorComponent) DeleteToLineEnd() {
m.textarea.DeleteToLineEnd()
}

func (m *editorComponent) DeleteToLineStart() {
m.textarea.DeleteToLineStart()
}

func (m *editorComponent) InsertLineBelow(copyIndent bool) {
m.textarea.InsertLineBelow(copyIndent)
}

func (m *editorComponent) InsertLineAbove(copyIndent bool) {
m.textarea.InsertLineAbove(copyIndent)
}

func (m *editorComponent) DeleteCurrentLine() string {
return m.textarea.DeleteCurrentLine()
}

func (m *editorComponent) BeginLineSelection() {
m.textarea.BeginLineSelection()
}

func (m *editorComponent) IsCursorAtEnd() bool {
return m.textarea.IsCursorAtEnd()
}

func getMediaTypeFromExtension(ext string) string {
switch strings.ToLower(ext) {
case ".jpg":
Expand Down
87 changes: 87 additions & 0 deletions packages/tui/internal/components/chat/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ type MessagesComponent interface {
PageDown() (tea.Model, tea.Cmd)
HalfPageUp() (tea.Model, tea.Cmd)
HalfPageDown() (tea.Model, tea.Cmd)
LineUp() (tea.Model, tea.Cmd)
LineDown() (tea.Model, tea.Cmd)
MoveLeft() (tea.Model, tea.Cmd)
MoveRight() (tea.Model, tea.Cmd)
PrevUserMessage() (tea.Model, tea.Cmd)
NextUserMessage() (tea.Model, tea.Cmd)
ToolDetailsVisible() bool
ThinkingBlocksVisible() bool
GotoTop() (tea.Model, tea.Cmd)
Expand Down Expand Up @@ -1080,6 +1086,87 @@ func (m *messagesComponent) HalfPageDown() (tea.Model, tea.Cmd) {
return m, nil
}

func (m *messagesComponent) LineUp() (tea.Model, tea.Cmd) {
step := m.app.ScrollSpeed
if step < 1 {
step = 3
}
m.viewport.LineUp(step)
return m, nil
}

func (m *messagesComponent) LineDown() (tea.Model, tea.Cmd) {
step := m.app.ScrollSpeed
if step < 1 {
step = 3
}
m.viewport.LineDown(step)
return m, nil
}

func (m *messagesComponent) MoveLeft() (tea.Model, tea.Cmd) {
m.viewport.MoveLeft(1)
return m, nil
}

func (m *messagesComponent) MoveRight() (tea.Model, tea.Cmd) {
m.viewport.MoveRight(1)
return m, nil
}

func (m *messagesComponent) PrevUserMessage() (tea.Model, tea.Cmd) {
if len(m.app.Messages) == 0 {
return m, nil
}
current := m.viewport.YOffset
target := -1
for i := len(m.app.Messages) - 1; i >= 0; i-- {
message := m.app.Messages[i]
user, ok := message.Info.(opencode.UserMessage)
if !ok {
continue
}
pos, exists := m.messagePositions[user.ID]
if !exists {
continue
}
if pos < current {
target = pos
break
}
}
if target >= 0 {
m.viewport.SetYOffset(target)
}
return m, nil
}

func (m *messagesComponent) NextUserMessage() (tea.Model, tea.Cmd) {
if len(m.app.Messages) == 0 {
return m, nil
}
current := m.viewport.YOffset
target := -1
for _, message := range m.app.Messages {
user, ok := message.Info.(opencode.UserMessage)
if !ok {
continue
}
pos, exists := m.messagePositions[user.ID]
if !exists {
continue
}
if pos > current {
target = pos
break
}
}
if target >= 0 {
m.viewport.SetYOffset(target)
}
return m, nil
}

func (m *messagesComponent) ToolDetailsVisible() bool {
return m.showToolDetails
}
Expand Down
Loading