feat(tui): composer soft-wrap, alt+backspace, bracketed paste, borderless input
- editorLines now soft-wraps long logical lines at the box width (caret tracked across wrapped rows) instead of overflowing/truncating - alt+backspace deletes the word before the cursor (deleteWordBack) - handle tea.PasteMsg so bracketed paste (Ctrl+V / Ctrl+Shift+V / right-click) drops clipboard text into the focused composer; was silently dropped - render the composer (in-session + launcher) borderless: top rule + flush text, no side borders or trailing padding, so a terminal drag-copy yields just the typed text instead of box chrome and whitespace
This commit is contained in:
@@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -120,6 +121,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.applyTasksLoaded(msg)
|
||||
return m, nil
|
||||
|
||||
case tea.PasteMsg:
|
||||
// Bracketed paste (Ctrl+V / Ctrl+Shift+V / right-click): drop the clipboard text into
|
||||
// whichever composer is focused. Without this the paste is silently swallowed.
|
||||
return m.handlePaste(msg.Content), m.tickKick()
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
next, cmd := m.handleKey(toKeyMsg(msg))
|
||||
// A keypress may have entered a typing/active state (insert mode, steering, …);
|
||||
@@ -142,6 +148,9 @@ func (m *Model) applySessionsLoaded(msg sessionsLoadedMsg) {
|
||||
}
|
||||
m.sessionListErr = ""
|
||||
m.sessionList = msg.sessions
|
||||
sort.Slice(m.sessionList, func(i, j int) bool {
|
||||
return m.sessionList[i].LastActivityAt > m.sessionList[j].LastActivityAt
|
||||
})
|
||||
if m.sessionListIndex >= len(m.sessionList) {
|
||||
m.sessionListIndex = 0
|
||||
}
|
||||
@@ -473,7 +482,11 @@ func (m Model) handleInsertKey(k keyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
case keyBackspace:
|
||||
m.backspace()
|
||||
if k.Alt {
|
||||
m.deleteWordBack()
|
||||
} else {
|
||||
m.backspace()
|
||||
}
|
||||
m.syncFilter()
|
||||
case keyLeft:
|
||||
if m.inputCursor > 0 {
|
||||
@@ -1210,6 +1223,20 @@ func (m Model) execPalette(id string) (tea.Model, tea.Cmd) {
|
||||
|
||||
// --- input editing ---
|
||||
|
||||
// handlePaste inserts pasted clipboard text into the focused composer (steering note or the
|
||||
// main input). Newlines are preserved — the input buffer is multi-line and editorLines wraps it.
|
||||
func (m Model) handlePaste(s string) tea.Model {
|
||||
switch {
|
||||
case s == "":
|
||||
case m.steering:
|
||||
m.steerBuffer += s
|
||||
case m.editMode == ModeInsert && m.overlay == OverlayNone:
|
||||
m.appendRunes(s)
|
||||
m.syncFilter()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) appendRunes(s string) {
|
||||
b := m.inputBuffer
|
||||
c := m.inputCursor
|
||||
@@ -1229,6 +1256,24 @@ func (m *Model) backspace() {
|
||||
m.inputCursor = c - 1
|
||||
}
|
||||
|
||||
// deleteWordBack removes the word before the cursor (Alt+Backspace): first any run of
|
||||
// spaces, then the run of non-space chars, all left of the cursor.
|
||||
func (m *Model) deleteWordBack() {
|
||||
c := m.inputCursor
|
||||
if c > len(m.inputBuffer) {
|
||||
c = len(m.inputBuffer)
|
||||
}
|
||||
i := c
|
||||
for i > 0 && m.inputBuffer[i-1] == ' ' {
|
||||
i--
|
||||
}
|
||||
for i > 0 && m.inputBuffer[i-1] != ' ' {
|
||||
i--
|
||||
}
|
||||
m.inputBuffer = m.inputBuffer[:i] + m.inputBuffer[c:]
|
||||
m.inputCursor = i
|
||||
}
|
||||
|
||||
// cycleLauncherWf advances the idle launch target: 0 = chat, 1..N = workflows[i-1], wrapping.
|
||||
func (m *Model) cycleLauncherWf() {
|
||||
m.launcherWf = (m.launcherWf + 1) % (len(m.workflows) + 1)
|
||||
|
||||
Reference in New Issue
Block a user