on main help sign in
remove edit/normal mode
commit2c5003e3fc94766cf848962ea0ffe94875c35d2b authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 23 Apr 2025, 12:08 (2 years ago) parent1b22acb
10 files changed+92−116
README.md+1−1 internal/tui/components/chat/editor.go+23−41 internal/tui/components/chat/list.go+41−22 internal/tui/components/dialog/commands.go−2 internal/tui/components/dialog/help.go+1−1 internal/tui/components/dialog/init.go+3−9 internal/tui/components/dialog/permission.go+5−9 internal/tui/components/dialog/session.go+2−2 internal/tui/page/chat.go+11−19 internal/tui/tui.go+5−10
README.md+1−1
@@ -1,4 +1,4 @@
1−# OpenCode
1+# ⌬ OpenCode
22
33> **⚠️ Early Development Notice:** This project is in early development and is not yet ready for production use. Features may change, break, or be incomplete. Use at your own risk.
44
internal/tui/components/chat/editor.go+23−41
@@ -26,7 +26,6 @@ type FocusEditorMsg bool
2626type focusedEditorKeyMaps struct {
2727 Send key.Binding
2828 OpenEditor key.Binding
29− Blur key.Binding
3029}
3130
3231type bluredEditorKeyMaps struct {
@@ -35,30 +34,11 @@ type bluredEditorKeyMaps struct {
3534 OpenEditor key.Binding
3635}
3736
38−var focusedKeyMaps = focusedEditorKeyMaps{
37+var KeyMaps = focusedEditorKeyMaps{
3938 Send: key.NewBinding(
4039 key.WithKeys("ctrl+s"),
4140 key.WithHelp("ctrl+s", "send message"),
4241 ),
43− Blur: key.NewBinding(
44− key.WithKeys("esc"),
45− key.WithHelp("esc", "focus messages"),
46− ),
47− OpenEditor: key.NewBinding(
48− key.WithKeys("ctrl+e"),
49− key.WithHelp("ctrl+e", "open editor"),
50− ),
51−}
52−
53−var bluredKeyMaps = bluredEditorKeyMaps{
54− Send: key.NewBinding(
55− key.WithKeys("ctrl+s", "enter"),
56− key.WithHelp("ctrl+s/enter", "send message"),
57− ),
58− Focus: key.NewBinding(
59− key.WithKeys("i"),
60− key.WithHelp("i", "focus editor"),
61− ),
6242 OpenEditor: key.NewBinding(
6343 key.WithKeys("ctrl+e"),
6444 key.WithHelp("ctrl+e", "open editor"),
@@ -88,6 +68,9 @@ func openEditor() tea.Cmd {
8868 if err != nil {
8969 return util.ReportError(err)
9070 }
71+ if len(content) == 0 {
72+ return util.ReportWarn("Message is empty")
73+ }
9174 os.Remove(tmpfile.Name())
9275 return SendMsg{
9376 Text: string(content),
@@ -106,7 +89,6 @@ func (m *editorCmp) send() tea.Cmd {
10689
10790 value := m.textarea.Value()
10891 m.textarea.Reset()
109− m.textarea.Blur()
11092 if value == "" {
11193 return nil
11294 }
@@ -131,26 +113,32 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
131113 return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
132114 }
133115 case tea.KeyMsg:
134− if key.Matches(msg, focusedKeyMaps.OpenEditor) {
116+ if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
117+ key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
118+ return m, nil
119+ }
120+ if key.Matches(msg, KeyMaps.OpenEditor) {
135121 if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
136122 return m, util.ReportWarn("Agent is working, please wait...")
137123 }
138124 return m, openEditor()
139125 }
140126 // if the key does not match any binding, return
141− if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) {
127+ if m.textarea.Focused() && key.Matches(msg, KeyMaps.Send) {
142128 return m, m.send()
143129 }
144− if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) {
145− return m, m.send()
146− }
147− if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Blur) {
148− m.textarea.Blur()
149− return m, util.CmdHandler(EditorFocusMsg(false))
150− }
151− if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) {
152− m.textarea.Focus()
153− return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
130+
131+ // Handle Enter key
132+ if m.textarea.Focused() && msg.String() == "enter" {
133+ value := m.textarea.Value()
134+ if len(value) > 0 && value[len(value)-1] == '\\' {
135+ // If the last character is a backslash, remove it and add a newline
136+ m.textarea.SetValue(value[:len(value)-1] + "\n")
137+ return m, nil
138+ } else {
139+ // Otherwise, send the message
140+ return m, m.send()
141+ }
154142 }
155143 }
156144 m.textarea, cmd = m.textarea.Update(msg)
@@ -175,13 +163,7 @@ func (m *editorCmp) GetSize() (int, int) {
175163
176164func (m *editorCmp) BindingKeys() []key.Binding {
177165 bindings := []key.Binding{}
178− if m.textarea.Focused() {
179− bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
180− } else {
181− bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
182− }
183−
184− bindings = append(bindings, layout.KeyMapToSlice(m.textarea.KeyMap)...)
166+ bindings = append(bindings, layout.KeyMapToSlice(KeyMaps)...)
185167 return bindings
186168}
187169
internal/tui/components/chat/list.go+41−22
@@ -14,7 +14,6 @@ import (
1414 "github.com/kujtimiihoxha/opencode/internal/message"
1515 "github.com/kujtimiihoxha/opencode/internal/pubsub"
1616 "github.com/kujtimiihoxha/opencode/internal/session"
17− "github.com/kujtimiihoxha/opencode/internal/tui/layout"
1817 "github.com/kujtimiihoxha/opencode/internal/tui/styles"
1918 "github.com/kujtimiihoxha/opencode/internal/tui/util"
2019)
@@ -26,7 +25,6 @@ type cacheItem struct {
2625type messagesCmp struct {
2726 app *app.App
2827 width, height int
29− writingMode bool
3028 viewport viewport.Model
3129 session session.Session
3230 messages []message.Message
@@ -38,6 +36,32 @@ type messagesCmp struct {
3836}
3937type renderFinishedMsg struct{}
4038
39+type MessageKeys struct {
40+ PageDown key.Binding
41+ PageUp key.Binding
42+ HalfPageUp key.Binding
43+ HalfPageDown key.Binding
44+}
45+
46+var messageKeys = MessageKeys{
47+ PageDown: key.NewBinding(
48+ key.WithKeys("pgdown"),
49+ key.WithHelp("f/pgdn", "page down"),
50+ ),
51+ PageUp: key.NewBinding(
52+ key.WithKeys("pgup"),
53+ key.WithHelp("b/pgup", "page up"),
54+ ),
55+ HalfPageUp: key.NewBinding(
56+ key.WithKeys("ctrl+u"),
57+ key.WithHelp("ctrl+u", "½ page up"),
58+ ),
59+ HalfPageDown: key.NewBinding(
60+ key.WithKeys("ctrl+d", "ctrl+d"),
61+ key.WithHelp("ctrl+d", "½ page down"),
62+ ),
63+}
64+
4165func (m *messagesCmp) Init() tea.Cmd {
4266 return tea.Batch(m.viewport.Init(), m.spinner.Tick)
4367}
@@ -45,8 +69,7 @@ func (m *messagesCmp) Init() tea.Cmd {
4569func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4670 var cmds []tea.Cmd
4771 switch msg := msg.(type) {
48− case EditorFocusMsg:
49− m.writingMode = bool(msg)
72+
5073 case SessionSelectedMsg:
5174 if msg.ID != m.session.ID {
5275 cmd := m.SetSession(msg)
@@ -63,10 +86,6 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6386 case renderFinishedMsg:
6487 m.rendering = false
6588 m.viewport.GotoBottom()
66− case tea.KeyMsg:
67− if m.writingMode {
68− return m, nil
69− }
7089 case pubsub.Event[message.Message]:
7190 needsRerender := false
7291 if msg.Type == pubsub.CreatedEvent {
@@ -326,22 +345,14 @@ func (m *messagesCmp) working() string {
326345func (m *messagesCmp) help() string {
327346 text := ""
328347
329− if m.writingMode {
348+ if m.app.CoderAgent.IsBusy() {
330349 text += lipgloss.JoinHorizontal(
331350 lipgloss.Left,
332351 styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
333352 styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
334− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
335− )
336− } else {
337− text += lipgloss.JoinHorizontal(
338− lipgloss.Left,
339− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
340− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
341− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to start writing"),
353+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"),
342354 )
343355 }
344−
345356 return styles.BaseStyle.
346357 Width(m.width).
347358 Render(text)
@@ -398,18 +409,26 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
398409}
399410
400411func (m *messagesCmp) BindingKeys() []key.Binding {
401− bindings := layout.KeyMapToSlice(m.viewport.KeyMap)
402− return bindings
412+ return []key.Binding{
413+ m.viewport.KeyMap.PageDown,
414+ m.viewport.KeyMap.PageUp,
415+ m.viewport.KeyMap.HalfPageUp,
416+ m.viewport.KeyMap.HalfPageDown,
417+ }
403418}
404419
405420func NewMessagesCmp(app *app.App) tea.Model {
406421 s := spinner.New()
407422 s.Spinner = spinner.Pulse
423+ vp := viewport.New(0, 0)
424+ vp.KeyMap.PageUp = messageKeys.PageUp
425+ vp.KeyMap.PageDown = messageKeys.PageDown
426+ vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
427+ vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
408428 return &messagesCmp{
409429 app: app,
410− writingMode: true,
411430 cachedContent: make(map[string]cacheItem),
412− viewport: viewport.New(0, 0),
431+ viewport: vp,
413432 spinner: s,
414433 }
415434}
internal/tui/components/dialog/commands.go−2
@@ -190,7 +190,6 @@ func (c *commandDialogCmp) View() string {
190190 styles.BaseStyle.Width(maxWidth).Render(""),
191191 styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
192192 styles.BaseStyle.Width(maxWidth).Render(""),
193− styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
194193 )
195194
196195 return styles.BaseStyle.Padding(1, 2).
@@ -244,4 +243,3 @@ func NewCommandDialogCmp() CommandDialog {
244243 selectedCommandID: "",
245244 }
246245}
247−
internal/tui/components/dialog/help.go+1−1
@@ -62,7 +62,7 @@ func (h *helpCmp) render() string {
6262 var (
6363 pairs []string
6464 width int
65− rows = 14 - 2
65+ rows = 10 - 2
6666 )
6767 for i := 0; i < len(bindings); i += rows {
6868 var (
internal/tui/components/dialog/init.go+3−9
@@ -46,8 +46,8 @@ func (k initDialogKeyMap) ShortHelp() []key.Binding {
4646 key.WithHelp("enter", "confirm"),
4747 ),
4848 key.NewBinding(
49− key.WithKeys("esc"),
50− key.WithHelp("esc", "cancel"),
49+ key.WithKeys("esc", "q"),
50+ key.WithHelp("esc/q", "cancel"),
5151 ),
5252 key.NewBinding(
5353 key.WithKeys("y", "n"),
@@ -114,6 +114,7 @@ func (m InitDialogCmp) View() string {
114114 Padding(1, 1).
115115 Render("Would you like to initialize this project?")
116116
117+ maxWidth = min(maxWidth, m.width-10)
117118 yesStyle := styles.BaseStyle
118119 noStyle := styles.BaseStyle
119120
@@ -144,12 +145,6 @@ func (m InitDialogCmp) View() string {
144145 Padding(1, 0).
145146 Render(buttons)
146147
147− help := styles.BaseStyle.
148− Width(maxWidth).
149− Padding(0, 1).
150− Foreground(styles.ForgroundDim).
151− Render("tab/←/→: toggle y/n: yes/no enter: confirm esc: cancel")
152−
153148 content := lipgloss.JoinVertical(
154149 lipgloss.Left,
155150 title,
@@ -158,7 +153,6 @@ func (m InitDialogCmp) View() string {
158153 question,
159154 buttons,
160155 styles.BaseStyle.Width(maxWidth).Render(""),
161− help,
162156 )
163157
164158 return styles.BaseStyle.Padding(1, 2).
internal/tui/components/dialog/permission.go+5−9
@@ -64,15 +64,15 @@ var permissionsKeys = permissionsMapping{
6464 ),
6565 Allow: key.NewBinding(
6666 key.WithKeys("a"),
67− key.WithHelp("a", "allow"),
67+ key.WithHelp("a", "[a]llow"),
6868 ),
6969 AllowSession: key.NewBinding(
70− key.WithKeys("A"),
71− key.WithHelp("A", "allow for session"),
70+ key.WithKeys("s"),
71+ key.WithHelp("s", "allow for [s]ession"),
7272 ),
7373 Deny: key.NewBinding(
7474 key.WithKeys("d"),
75− key.WithHelp("d", "deny"),
75+ key.WithHelp("d", "[d]eny"),
7676 ),
7777 Tab: key.NewBinding(
7878 key.WithKeys("tab"),
@@ -375,9 +375,6 @@ func (p *permissionDialogCmp) render() string {
375375 contentFinal = p.renderDefaultContent()
376376 }
377377
378− // Add help text
379− helpText := styles.BaseStyle.Width(p.width - 4).Padding(0, 1).Foreground(styles.ForgroundDim).Render("←/→/tab: switch options a: allow A: allow for session d: deny enter/space: confirm")
380−
381378 content := lipgloss.JoinVertical(
382379 lipgloss.Top,
383380 title,
@@ -385,8 +382,7 @@ func (p *permissionDialogCmp) render() string {
385382 headerContent,
386383 contentFinal,
387384 buttons,
388− styles.BaseStyle.Render(strings.Repeat(" ", p.width - 4)),
389− helpText,
385+ styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)),
390386 )
391387
392388 return styles.BaseStyle.
internal/tui/components/dialog/session.go+2−2
@@ -122,6 +122,8 @@ func (s *sessionDialogCmp) View() string {
122122 }
123123 }
124124
125+ maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
126+
125127 // Limit height to avoid taking up too much screen space
126128 maxVisibleSessions := min(10, len(s.sessions))
127129
@@ -169,7 +171,6 @@ func (s *sessionDialogCmp) View() string {
169171 styles.BaseStyle.Width(maxWidth).Render(""),
170172 styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
171173 styles.BaseStyle.Width(maxWidth).Render(""),
172− styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
173174 )
174175
175176 return styles.BaseStyle.Padding(1, 2).
@@ -223,4 +224,3 @@ func NewSessionDialogCmp() SessionDialog {
223224 selectedSessionID: "",
224225 }
225226}
226−
internal/tui/page/chat.go+11−19
@@ -15,12 +15,11 @@ import (
1515var ChatPage PageID = "chat"
1616
1717type chatPage struct {
18− app *app.App
19− editor layout.Container
20− messages layout.Container
21− layout layout.SplitPaneLayout
22− session session.Session
23− editingMode bool
18+ app *app.App
19+ editor layout.Container
20+ messages layout.Container
21+ layout layout.SplitPaneLayout
22+ session session.Session
2423}
2524
2625type ChatKeyMap struct {
@@ -34,8 +33,8 @@ var keyMap = ChatKeyMap{
3433 key.WithHelp("ctrl+n", "new session"),
3534 ),
3635 Cancel: key.NewBinding(
37− key.WithKeys("ctrl+x"),
38− key.WithHelp("ctrl+x", "cancel"),
36+ key.WithKeys("esc"),
37+ key.WithHelp("esc", "cancel"),
3938 ),
4039}
4140
@@ -65,8 +64,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6564 }
6665 }
6766 p.session = msg
68− case chat.EditorFocusMsg:
69− p.editingMode = bool(msg)
7067 case tea.KeyMsg:
7168 switch {
7269 case key.Matches(msg, keyMap.NewSession):
@@ -136,11 +133,7 @@ func (p *chatPage) View() string {
136133
137134func (p *chatPage) BindingKeys() []key.Binding {
138135 bindings := layout.KeyMapToSlice(keyMap)
139− if p.editingMode {
140− bindings = append(bindings, p.editor.BindingKeys()...)
141− } else {
142− bindings = append(bindings, p.messages.BindingKeys()...)
143− }
136+ bindings = append(bindings, p.messages.BindingKeys()...)
144137 return bindings
145138}
146139
@@ -155,10 +148,9 @@ func NewChatPage(app *app.App) tea.Model {
155148 layout.WithBorder(true, false, false, false),
156149 )
157150 return &chatPage{
158− app: app,
159− editor: editorContainer,
160− messages: messagesContainer,
161− editingMode: true,
151+ app: app,
152+ editor: editorContainer,
153+ messages: messagesContainer,
162154 layout: layout.NewSplitPane(
163155 layout.WithLeftPanel(messagesContainer),
164156 layout.WithBottomPanel(editorContainer),
internal/tui/tui.go+5−10
@@ -30,7 +30,7 @@ type keyMap struct {
3030var keys = keyMap{
3131 Logs: key.NewBinding(
3232 key.WithKeys("ctrl+l"),
33− key.WithHelp("ctrl+L", "logs"),
33+ key.WithHelp("ctrl+l", "logs"),
3434 ),
3535
3636 Quit: key.NewBinding(
@@ -49,7 +49,7 @@ var keys = keyMap{
4949
5050 Commands: key.NewBinding(
5151 key.WithKeys("ctrl+k"),
52− key.WithHelp("ctrl+K", "commands"),
52+ key.WithHelp("ctrl+k", "commands"),
5353 ),
5454}
5555
@@ -95,8 +95,6 @@ type appModel struct {
9595
9696 showInitDialog bool
9797 initDialog dialog.InitDialogCmp
98−
99− editingMode bool
10098}
10199
102100func (a appModel) Init() tea.Cmd {
@@ -164,8 +162,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
164162 a.initDialog.SetSize(msg.Width, msg.Height)
165163
166164 return a, tea.Batch(cmds...)
167− case chat.EditorFocusMsg:
168− a.editingMode = bool(msg)
169165 // Status
170166 case util.InfoMsg:
171167 s, cmd := a.status.Update(msg)
@@ -360,7 +356,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
360356 a.showHelp = !a.showHelp
361357 return a, nil
362358 case key.Matches(msg, helpEsc):
363− if !a.editingMode {
359+ if a.app.CoderAgent.IsBusy() {
364360 if a.showQuit {
365361 return a, nil
366362 }
@@ -477,7 +473,7 @@ func (a appModel) View() string {
477473 )
478474 }
479475
480− if a.editingMode {
476+ if !a.app.CoderAgent.IsBusy() {
481477 a.status.SetHelpMsg("ctrl+? help")
482478 } else {
483479 a.status.SetHelpMsg("? help")
@@ -494,7 +490,7 @@ func (a appModel) View() string {
494490 if a.currentPage == page.LogsPage {
495491 bindings = append(bindings, logsKeyReturnKey)
496492 }
497− if !a.editingMode {
493+ if !a.app.CoderAgent.IsBusy() {
498494 bindings = append(bindings, helpEsc)
499495 }
500496 a.help.SetBindings(bindings)
@@ -585,7 +581,6 @@ func New(app *app.App) tea.Model {
585581 permissions: dialog.NewPermissionDialogCmp(),
586582 initDialog: dialog.NewInitDialogCmp(),
587583 app: app,
588− editingMode: true,
589584 commands: []dialog.Command{},
590585 pages: map[page.PageID]tea.Model{
591586 page.ChatPage: page.NewChatPage(app),