README.md 🔗
@@ -1,4 +1,4 @@
-# OpenCode
+# ⌬ OpenCode
> **⚠️ 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.
Kujtim Hoxha created
README.md | 2
internal/tui/components/chat/editor.go | 64 +++++++--------------
internal/tui/components/chat/list.go | 63 ++++++++++++++-------
internal/tui/components/dialog/commands.go | 2
internal/tui/components/dialog/help.go | 2
internal/tui/components/dialog/init.go | 12 +---
internal/tui/components/dialog/permission.go | 14 +---
internal/tui/components/dialog/session.go | 4
internal/tui/page/chat.go | 30 +++------
internal/tui/tui.go | 15 +---
10 files changed, 92 insertions(+), 116 deletions(-)
@@ -1,4 +1,4 @@
-# OpenCode
+# ⌬ OpenCode
> **⚠️ 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.
@@ -26,7 +26,6 @@ type FocusEditorMsg bool
type focusedEditorKeyMaps struct {
Send key.Binding
OpenEditor key.Binding
- Blur key.Binding
}
type bluredEditorKeyMaps struct {
@@ -35,30 +34,11 @@ type bluredEditorKeyMaps struct {
OpenEditor key.Binding
}
-var focusedKeyMaps = focusedEditorKeyMaps{
+var KeyMaps = focusedEditorKeyMaps{
Send: key.NewBinding(
key.WithKeys("ctrl+s"),
key.WithHelp("ctrl+s", "send message"),
),
- Blur: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "focus messages"),
- ),
- OpenEditor: key.NewBinding(
- key.WithKeys("ctrl+e"),
- key.WithHelp("ctrl+e", "open editor"),
- ),
-}
-
-var bluredKeyMaps = bluredEditorKeyMaps{
- Send: key.NewBinding(
- key.WithKeys("ctrl+s", "enter"),
- key.WithHelp("ctrl+s/enter", "send message"),
- ),
- Focus: key.NewBinding(
- key.WithKeys("i"),
- key.WithHelp("i", "focus editor"),
- ),
OpenEditor: key.NewBinding(
key.WithKeys("ctrl+e"),
key.WithHelp("ctrl+e", "open editor"),
@@ -88,6 +68,9 @@ func openEditor() tea.Cmd {
if err != nil {
return util.ReportError(err)
}
+ if len(content) == 0 {
+ return util.ReportWarn("Message is empty")
+ }
os.Remove(tmpfile.Name())
return SendMsg{
Text: string(content),
@@ -106,7 +89,6 @@ func (m *editorCmp) send() tea.Cmd {
value := m.textarea.Value()
m.textarea.Reset()
- m.textarea.Blur()
if value == "" {
return nil
}
@@ -131,26 +113,32 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
}
case tea.KeyMsg:
- if key.Matches(msg, focusedKeyMaps.OpenEditor) {
+ if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
+ key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
+ return m, nil
+ }
+ if key.Matches(msg, KeyMaps.OpenEditor) {
if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
return m, util.ReportWarn("Agent is working, please wait...")
}
return m, openEditor()
}
// if the key does not match any binding, return
- if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) {
+ if m.textarea.Focused() && key.Matches(msg, KeyMaps.Send) {
return m, m.send()
}
- if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) {
- return m, m.send()
- }
- if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Blur) {
- m.textarea.Blur()
- return m, util.CmdHandler(EditorFocusMsg(false))
- }
- if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) {
- m.textarea.Focus()
- return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
+
+ // Handle Enter key
+ if m.textarea.Focused() && msg.String() == "enter" {
+ value := m.textarea.Value()
+ if len(value) > 0 && value[len(value)-1] == '\\' {
+ // If the last character is a backslash, remove it and add a newline
+ m.textarea.SetValue(value[:len(value)-1] + "\n")
+ return m, nil
+ } else {
+ // Otherwise, send the message
+ return m, m.send()
+ }
}
}
m.textarea, cmd = m.textarea.Update(msg)
@@ -175,13 +163,7 @@ func (m *editorCmp) GetSize() (int, int) {
func (m *editorCmp) BindingKeys() []key.Binding {
bindings := []key.Binding{}
- if m.textarea.Focused() {
- bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
- } else {
- bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
- }
-
- bindings = append(bindings, layout.KeyMapToSlice(m.textarea.KeyMap)...)
+ bindings = append(bindings, layout.KeyMapToSlice(KeyMaps)...)
return bindings
}
@@ -14,7 +14,6 @@ import (
"github.com/kujtimiihoxha/opencode/internal/message"
"github.com/kujtimiihoxha/opencode/internal/pubsub"
"github.com/kujtimiihoxha/opencode/internal/session"
- "github.com/kujtimiihoxha/opencode/internal/tui/layout"
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
"github.com/kujtimiihoxha/opencode/internal/tui/util"
)
@@ -26,7 +25,6 @@ type cacheItem struct {
type messagesCmp struct {
app *app.App
width, height int
- writingMode bool
viewport viewport.Model
session session.Session
messages []message.Message
@@ -38,6 +36,32 @@ type messagesCmp struct {
}
type renderFinishedMsg struct{}
+type MessageKeys struct {
+ PageDown key.Binding
+ PageUp key.Binding
+ HalfPageUp key.Binding
+ HalfPageDown key.Binding
+}
+
+var messageKeys = MessageKeys{
+ PageDown: key.NewBinding(
+ key.WithKeys("pgdown"),
+ key.WithHelp("f/pgdn", "page down"),
+ ),
+ PageUp: key.NewBinding(
+ key.WithKeys("pgup"),
+ key.WithHelp("b/pgup", "page up"),
+ ),
+ HalfPageUp: key.NewBinding(
+ key.WithKeys("ctrl+u"),
+ key.WithHelp("ctrl+u", "½ page up"),
+ ),
+ HalfPageDown: key.NewBinding(
+ key.WithKeys("ctrl+d", "ctrl+d"),
+ key.WithHelp("ctrl+d", "½ page down"),
+ ),
+}
+
func (m *messagesCmp) Init() tea.Cmd {
return tea.Batch(m.viewport.Init(), m.spinner.Tick)
}
@@ -45,8 +69,7 @@ func (m *messagesCmp) Init() tea.Cmd {
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
- case EditorFocusMsg:
- m.writingMode = bool(msg)
+
case SessionSelectedMsg:
if msg.ID != m.session.ID {
cmd := m.SetSession(msg)
@@ -63,10 +86,6 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case renderFinishedMsg:
m.rendering = false
m.viewport.GotoBottom()
- case tea.KeyMsg:
- if m.writingMode {
- return m, nil
- }
case pubsub.Event[message.Message]:
needsRerender := false
if msg.Type == pubsub.CreatedEvent {
@@ -326,22 +345,14 @@ func (m *messagesCmp) working() string {
func (m *messagesCmp) help() string {
text := ""
- if m.writingMode {
+ if m.app.CoderAgent.IsBusy() {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
- )
- } else {
- text += lipgloss.JoinHorizontal(
- lipgloss.Left,
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
- styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to start writing"),
+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"),
)
}
-
return styles.BaseStyle.
Width(m.width).
Render(text)
@@ -398,18 +409,26 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
}
func (m *messagesCmp) BindingKeys() []key.Binding {
- bindings := layout.KeyMapToSlice(m.viewport.KeyMap)
- return bindings
+ return []key.Binding{
+ m.viewport.KeyMap.PageDown,
+ m.viewport.KeyMap.PageUp,
+ m.viewport.KeyMap.HalfPageUp,
+ m.viewport.KeyMap.HalfPageDown,
+ }
}
func NewMessagesCmp(app *app.App) tea.Model {
s := spinner.New()
s.Spinner = spinner.Pulse
+ vp := viewport.New(0, 0)
+ vp.KeyMap.PageUp = messageKeys.PageUp
+ vp.KeyMap.PageDown = messageKeys.PageDown
+ vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
+ vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
return &messagesCmp{
app: app,
- writingMode: true,
cachedContent: make(map[string]cacheItem),
- viewport: viewport.New(0, 0),
+ viewport: vp,
spinner: s,
}
}
@@ -190,7 +190,6 @@ func (c *commandDialogCmp) View() string {
styles.BaseStyle.Width(maxWidth).Render(""),
styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
styles.BaseStyle.Width(maxWidth).Render(""),
- styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
)
return styles.BaseStyle.Padding(1, 2).
@@ -244,4 +243,3 @@ func NewCommandDialogCmp() CommandDialog {
selectedCommandID: "",
}
}
-
@@ -62,7 +62,7 @@ func (h *helpCmp) render() string {
var (
pairs []string
width int
- rows = 14 - 2
+ rows = 10 - 2
)
for i := 0; i < len(bindings); i += rows {
var (
@@ -46,8 +46,8 @@ func (k initDialogKeyMap) ShortHelp() []key.Binding {
key.WithHelp("enter", "confirm"),
),
key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "cancel"),
+ key.WithKeys("esc", "q"),
+ key.WithHelp("esc/q", "cancel"),
),
key.NewBinding(
key.WithKeys("y", "n"),
@@ -114,6 +114,7 @@ func (m InitDialogCmp) View() string {
Padding(1, 1).
Render("Would you like to initialize this project?")
+ maxWidth = min(maxWidth, m.width-10)
yesStyle := styles.BaseStyle
noStyle := styles.BaseStyle
@@ -144,12 +145,6 @@ func (m InitDialogCmp) View() string {
Padding(1, 0).
Render(buttons)
- help := styles.BaseStyle.
- Width(maxWidth).
- Padding(0, 1).
- Foreground(styles.ForgroundDim).
- Render("tab/←/→: toggle y/n: yes/no enter: confirm esc: cancel")
-
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
@@ -158,7 +153,6 @@ func (m InitDialogCmp) View() string {
question,
buttons,
styles.BaseStyle.Width(maxWidth).Render(""),
- help,
)
return styles.BaseStyle.Padding(1, 2).
@@ -64,15 +64,15 @@ var permissionsKeys = permissionsMapping{
),
Allow: key.NewBinding(
key.WithKeys("a"),
- key.WithHelp("a", "allow"),
+ key.WithHelp("a", "[a]llow"),
),
AllowSession: key.NewBinding(
- key.WithKeys("A"),
- key.WithHelp("A", "allow for session"),
+ key.WithKeys("s"),
+ key.WithHelp("s", "allow for [s]ession"),
),
Deny: key.NewBinding(
key.WithKeys("d"),
- key.WithHelp("d", "deny"),
+ key.WithHelp("d", "[d]eny"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
@@ -375,9 +375,6 @@ func (p *permissionDialogCmp) render() string {
contentFinal = p.renderDefaultContent()
}
- // Add help text
- 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")
-
content := lipgloss.JoinVertical(
lipgloss.Top,
title,
@@ -385,8 +382,7 @@ func (p *permissionDialogCmp) render() string {
headerContent,
contentFinal,
buttons,
- styles.BaseStyle.Render(strings.Repeat(" ", p.width - 4)),
- helpText,
+ styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)),
)
return styles.BaseStyle.
@@ -122,6 +122,8 @@ func (s *sessionDialogCmp) View() string {
}
}
+ maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
+
// Limit height to avoid taking up too much screen space
maxVisibleSessions := min(10, len(s.sessions))
@@ -169,7 +171,6 @@ func (s *sessionDialogCmp) View() string {
styles.BaseStyle.Width(maxWidth).Render(""),
styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
styles.BaseStyle.Width(maxWidth).Render(""),
- styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
)
return styles.BaseStyle.Padding(1, 2).
@@ -223,4 +224,3 @@ func NewSessionDialogCmp() SessionDialog {
selectedSessionID: "",
}
}
-
@@ -15,12 +15,11 @@ import (
var ChatPage PageID = "chat"
type chatPage struct {
- app *app.App
- editor layout.Container
- messages layout.Container
- layout layout.SplitPaneLayout
- session session.Session
- editingMode bool
+ app *app.App
+ editor layout.Container
+ messages layout.Container
+ layout layout.SplitPaneLayout
+ session session.Session
}
type ChatKeyMap struct {
@@ -34,8 +33,8 @@ var keyMap = ChatKeyMap{
key.WithHelp("ctrl+n", "new session"),
),
Cancel: key.NewBinding(
- key.WithKeys("ctrl+x"),
- key.WithHelp("ctrl+x", "cancel"),
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel"),
),
}
@@ -65,8 +64,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
p.session = msg
- case chat.EditorFocusMsg:
- p.editingMode = bool(msg)
case tea.KeyMsg:
switch {
case key.Matches(msg, keyMap.NewSession):
@@ -136,11 +133,7 @@ func (p *chatPage) View() string {
func (p *chatPage) BindingKeys() []key.Binding {
bindings := layout.KeyMapToSlice(keyMap)
- if p.editingMode {
- bindings = append(bindings, p.editor.BindingKeys()...)
- } else {
- bindings = append(bindings, p.messages.BindingKeys()...)
- }
+ bindings = append(bindings, p.messages.BindingKeys()...)
return bindings
}
@@ -155,10 +148,9 @@ func NewChatPage(app *app.App) tea.Model {
layout.WithBorder(true, false, false, false),
)
return &chatPage{
- app: app,
- editor: editorContainer,
- messages: messagesContainer,
- editingMode: true,
+ app: app,
+ editor: editorContainer,
+ messages: messagesContainer,
layout: layout.NewSplitPane(
layout.WithLeftPanel(messagesContainer),
layout.WithBottomPanel(editorContainer),
@@ -30,7 +30,7 @@ type keyMap struct {
var keys = keyMap{
Logs: key.NewBinding(
key.WithKeys("ctrl+l"),
- key.WithHelp("ctrl+L", "logs"),
+ key.WithHelp("ctrl+l", "logs"),
),
Quit: key.NewBinding(
@@ -49,7 +49,7 @@ var keys = keyMap{
Commands: key.NewBinding(
key.WithKeys("ctrl+k"),
- key.WithHelp("ctrl+K", "commands"),
+ key.WithHelp("ctrl+k", "commands"),
),
}
@@ -95,8 +95,6 @@ type appModel struct {
showInitDialog bool
initDialog dialog.InitDialogCmp
-
- editingMode bool
}
func (a appModel) Init() tea.Cmd {
@@ -164,8 +162,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.initDialog.SetSize(msg.Width, msg.Height)
return a, tea.Batch(cmds...)
- case chat.EditorFocusMsg:
- a.editingMode = bool(msg)
// Status
case util.InfoMsg:
s, cmd := a.status.Update(msg)
@@ -360,7 +356,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.showHelp = !a.showHelp
return a, nil
case key.Matches(msg, helpEsc):
- if !a.editingMode {
+ if a.app.CoderAgent.IsBusy() {
if a.showQuit {
return a, nil
}
@@ -477,7 +473,7 @@ func (a appModel) View() string {
)
}
- if a.editingMode {
+ if !a.app.CoderAgent.IsBusy() {
a.status.SetHelpMsg("ctrl+? help")
} else {
a.status.SetHelpMsg("? help")
@@ -494,7 +490,7 @@ func (a appModel) View() string {
if a.currentPage == page.LogsPage {
bindings = append(bindings, logsKeyReturnKey)
}
- if !a.editingMode {
+ if !a.app.CoderAgent.IsBusy() {
bindings = append(bindings, helpEsc)
}
a.help.SetBindings(bindings)
@@ -585,7 +581,6 @@ func New(app *app.App) tea.Model {
permissions: dialog.NewPermissionDialogCmp(),
initDialog: dialog.NewInitDialogCmp(),
app: app,
- editingMode: true,
commands: []dialog.Command{},
pages: map[page.PageID]tea.Model{
page.ChatPage: page.NewChatPage(app),