diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-21 19:59:35 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-04-21 19:59:35 +0200 |
| commit | f33dff87725764af0b675b5e5b2e011b21c14c90 (patch) | |
| tree | 4fe2c022305f13775f2cab3cdd80cd808259765b /internal/tui/page | |
| parent | 6b1c64bcc75b89c530294b6a2d4404682b435d56 (diff) | |
| parent | 3a6a26981a8074b6ab0eaadb520db986e04799ff (diff) | |
| download | opencode-f33dff87725764af0b675b5e5b2e011b21c14c90.tar.gz opencode-f33dff87725764af0b675b5e5b2e011b21c14c90.zip | |
Merge pull request #27 from kujtimiihoxha/opencode
OpenCode - Initial Implementation
Diffstat (limited to 'internal/tui/page')
| -rw-r--r-- | internal/tui/page/chat.go | 167 | ||||
| -rw-r--r-- | internal/tui/page/init.go | 308 | ||||
| -rw-r--r-- | internal/tui/page/logs.go | 83 | ||||
| -rw-r--r-- | internal/tui/page/repl.go | 21 |
4 files changed, 240 insertions, 339 deletions
diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go new file mode 100644 index 000000000..a5a656a22 --- /dev/null +++ b/internal/tui/page/chat.go @@ -0,0 +1,167 @@ +package page + +import ( + "context" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/components/chat" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/util" +) + +var ChatPage PageID = "chat" + +type chatPage struct { + app *app.App + editor layout.Container + messages layout.Container + layout layout.SplitPaneLayout + session session.Session + editingMode bool +} + +type ChatKeyMap struct { + NewSession key.Binding + Cancel key.Binding +} + +var keyMap = ChatKeyMap{ + NewSession: key.NewBinding( + key.WithKeys("ctrl+n"), + key.WithHelp("ctrl+n", "new session"), + ), + Cancel: key.NewBinding( + key.WithKeys("ctrl+x"), + key.WithHelp("ctrl+x", "cancel"), + ), +} + +func (p *chatPage) Init() tea.Cmd { + cmds := []tea.Cmd{ + p.layout.Init(), + } + return tea.Batch(cmds...) +} + +func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + cmd := p.layout.SetSize(msg.Width, msg.Height) + cmds = append(cmds, cmd) + case chat.SendMsg: + cmd := p.sendMessage(msg.Text) + if cmd != nil { + return p, cmd + } + case chat.SessionSelectedMsg: + if p.session.ID == "" { + cmd := p.setSidebar() + if cmd != nil { + cmds = append(cmds, cmd) + } + } + p.session = msg + case chat.EditorFocusMsg: + p.editingMode = bool(msg) + case tea.KeyMsg: + switch { + case key.Matches(msg, keyMap.NewSession): + p.session = session.Session{} + return p, tea.Batch( + p.clearSidebar(), + util.CmdHandler(chat.SessionClearedMsg{}), + ) + case key.Matches(msg, keyMap.Cancel): + if p.session.ID != "" { + // Cancel the current session's generation process + // This allows users to interrupt long-running operations + p.app.CoderAgent.Cancel(p.session.ID) + return p, nil + } + } + } + u, cmd := p.layout.Update(msg) + cmds = append(cmds, cmd) + p.layout = u.(layout.SplitPaneLayout) + return p, tea.Batch(cmds...) +} + +func (p *chatPage) setSidebar() tea.Cmd { + sidebarContainer := layout.NewContainer( + chat.NewSidebarCmp(p.session, p.app.History), + layout.WithPadding(1, 1, 1, 1), + ) + return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init()) +} + +func (p *chatPage) clearSidebar() tea.Cmd { + return p.layout.ClearRightPanel() +} + +func (p *chatPage) sendMessage(text string) tea.Cmd { + var cmds []tea.Cmd + if p.session.ID == "" { + session, err := p.app.Sessions.Create(context.Background(), "New Session") + if err != nil { + return util.ReportError(err) + } + + p.session = session + cmd := p.setSidebar() + if cmd != nil { + cmds = append(cmds, cmd) + } + cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session))) + } + + p.app.CoderAgent.Run(context.Background(), p.session.ID, text) + return tea.Batch(cmds...) +} + +func (p *chatPage) SetSize(width, height int) tea.Cmd { + return p.layout.SetSize(width, height) +} + +func (p *chatPage) GetSize() (int, int) { + return p.layout.GetSize() +} + +func (p *chatPage) View() string { + return p.layout.View() +} + +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()...) + } + return bindings +} + +func NewChatPage(app *app.App) tea.Model { + messagesContainer := layout.NewContainer( + chat.NewMessagesCmp(app), + layout.WithPadding(1, 1, 0, 1), + ) + + editorContainer := layout.NewContainer( + chat.NewEditorCmp(app), + layout.WithBorder(true, false, false, false), + ) + return &chatPage{ + app: app, + editor: editorContainer, + messages: messagesContainer, + editingMode: true, + layout: layout.NewSplitPane( + layout.WithLeftPanel(messagesContainer), + layout.WithBottomPanel(editorContainer), + ), + } +} diff --git a/internal/tui/page/init.go b/internal/tui/page/init.go deleted file mode 100644 index 93a5e6fba..000000000 --- a/internal/tui/page/init.go +++ /dev/null @@ -1,308 +0,0 @@ -package page - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" - "github.com/spf13/viper" -) - -var InitPage PageID = "init" - -type configSaved struct{} - -type initPage struct { - form *huh.Form - width int - height int - saved bool - errorMsg string - statusMsg string - modelOpts []huh.Option[string] - bigModel string - smallModel string - openAIKey string - anthropicKey string - groqKey string - maxTokens string - dataDir string - agent string -} - -func (i *initPage) Init() tea.Cmd { - return i.form.Init() -} - -func (i *initPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - - switch msg := msg.(type) { - case tea.WindowSizeMsg: - i.width = msg.Width - 4 // Account for border - i.height = msg.Height - 4 - i.form = i.form.WithWidth(i.width).WithHeight(i.height) - return i, nil - - case configSaved: - i.saved = true - i.statusMsg = "Configuration saved successfully. Press any key to continue." - return i, nil - } - - if i.saved { - switch msg.(type) { - case tea.KeyMsg: - return i, util.CmdHandler(PageChangeMsg{ID: ReplPage}) - } - return i, nil - } - - // Process the form - form, cmd := i.form.Update(msg) - if f, ok := form.(*huh.Form); ok { - i.form = f - cmds = append(cmds, cmd) - } - - if i.form.State == huh.StateCompleted { - // Save configuration to file - configPath := filepath.Join(os.Getenv("HOME"), ".termai.yaml") - maxTokens, _ := strconv.Atoi(i.maxTokens) - config := map[string]any{ - "models": map[string]string{ - "big": i.bigModel, - "small": i.smallModel, - }, - "providers": map[string]any{ - "openai": map[string]string{ - "key": i.openAIKey, - }, - "anthropic": map[string]string{ - "key": i.anthropicKey, - }, - "groq": map[string]string{ - "key": i.groqKey, - }, - "common": map[string]int{ - "max_tokens": maxTokens, - }, - }, - "data": map[string]string{ - "dir": i.dataDir, - }, - "agents": map[string]string{ - "default": i.agent, - }, - "log": map[string]string{ - "level": "info", - }, - } - - // Write config to viper - for k, v := range config { - viper.Set(k, v) - } - - // Save configuration - err := viper.WriteConfigAs(configPath) - if err != nil { - i.errorMsg = fmt.Sprintf("Failed to save configuration: %s", err) - return i, nil - } - - // Return to main page - return i, util.CmdHandler(configSaved{}) - } - - return i, tea.Batch(cmds...) -} - -func (i *initPage) View() string { - if i.saved { - return lipgloss.NewStyle(). - Width(i.width). - Height(i.height). - Align(lipgloss.Center, lipgloss.Center). - Render(lipgloss.JoinVertical( - lipgloss.Center, - lipgloss.NewStyle().Foreground(styles.Green).Render("✓ Configuration Saved"), - "", - lipgloss.NewStyle().Foreground(styles.Blue).Render(i.statusMsg), - )) - } - - view := i.form.View() - if i.errorMsg != "" { - errorBox := lipgloss.NewStyle(). - Padding(1). - Border(lipgloss.RoundedBorder()). - BorderForeground(styles.Red). - Width(i.width - 4). - Render(i.errorMsg) - view = lipgloss.JoinVertical(lipgloss.Left, errorBox, view) - } - return view -} - -func (i *initPage) GetSize() (int, int) { - return i.width, i.height -} - -func (i *initPage) SetSize(width int, height int) { - i.width = width - i.height = height - i.form = i.form.WithWidth(width).WithHeight(height) -} - -func (i *initPage) BindingKeys() []key.Binding { - if i.saved { - return []key.Binding{ - key.NewBinding( - key.WithKeys("enter", "space", "esc"), - key.WithHelp("any key", "continue"), - ), - } - } - return i.form.KeyBinds() -} - -func NewInitPage() tea.Model { - // Create model options - var modelOpts []huh.Option[string] - for id, model := range models.SupportedModels { - modelOpts = append(modelOpts, huh.NewOption(model.Name, string(id))) - } - - // Create agent options - agentOpts := []huh.Option[string]{ - huh.NewOption("Coder", "coder"), - huh.NewOption("Assistant", "assistant"), - } - - // Init page with form - initModel := &initPage{ - modelOpts: modelOpts, - bigModel: string(models.Claude37Sonnet), - smallModel: string(models.Claude37Sonnet), - maxTokens: "4000", - dataDir: ".termai", - agent: "coder", - } - - // API Keys group - apiKeysGroup := huh.NewGroup( - huh.NewNote(). - Title("API Keys"). - Description("You need to provide at least one API key to use termai"), - - huh.NewInput(). - Title("OpenAI API Key"). - Placeholder("sk-..."). - Key("openai_key"). - Value(&initModel.openAIKey), - - huh.NewInput(). - Title("Anthropic API Key"). - Placeholder("sk-ant-..."). - Key("anthropic_key"). - Value(&initModel.anthropicKey), - - huh.NewInput(). - Title("Groq API Key"). - Placeholder("gsk_..."). - Key("groq_key"). - Value(&initModel.groqKey), - ) - - // Model configuration group - modelsGroup := huh.NewGroup( - huh.NewNote(). - Title("Model Configuration"). - Description("Select which models to use"), - - huh.NewSelect[string](). - Title("Big Model"). - Options(modelOpts...). - Key("big_model"). - Value(&initModel.bigModel), - - huh.NewSelect[string](). - Title("Small Model"). - Options(modelOpts...). - Key("small_model"). - Value(&initModel.smallModel), - - huh.NewInput(). - Title("Max Tokens"). - Placeholder("4000"). - Key("max_tokens"). - CharLimit(5). - Validate(func(s string) error { - var n int - _, err := fmt.Sscanf(s, "%d", &n) - if err != nil || n <= 0 { - return fmt.Errorf("must be a positive number") - } - initModel.maxTokens = s - return nil - }). - Value(&initModel.maxTokens), - ) - - // General settings group - generalGroup := huh.NewGroup( - huh.NewNote(). - Title("General Settings"). - Description("Configure general termai settings"), - - huh.NewInput(). - Title("Data Directory"). - Placeholder(".termai"). - Key("data_dir"). - Value(&initModel.dataDir), - - huh.NewSelect[string](). - Title("Default Agent"). - Options(agentOpts...). - Key("agent"). - Value(&initModel.agent), - - huh.NewConfirm(). - Title("Save Configuration"). - Affirmative("Save"). - Negative("Cancel"), - ) - - // Create form with theme - form := huh.NewForm( - apiKeysGroup, - modelsGroup, - generalGroup, - ).WithTheme(styles.HuhTheme()). - WithShowHelp(true). - WithShowErrors(true) - - // Set the form in the model - initModel.form = form - - return layout.NewSinglePane( - initModel, - layout.WithSinglePaneFocusable(true), - layout.WithSinglePaneBordered(true), - layout.WithSignlePaneBorderText( - map[layout.BorderPosition]string{ - layout.TopMiddleBorder: "Welcome to termai - Initial Setup", - }, - ), - ) -} diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go index 12afaf6aa..f0d35fb7b 100644 --- a/internal/tui/page/logs.go +++ b/internal/tui/page/logs.go @@ -1,20 +1,83 @@ package page import ( + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/tui/components/logs" - "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/charmbracelet/lipgloss" + "github.com/kujtimiihoxha/opencode/internal/tui/components/logs" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) var LogsPage PageID = "logs" -func NewLogsPage() tea.Model { - return layout.NewBentoLayout( - layout.BentoPanes{ - layout.BentoRightTopPane: logs.NewLogsTable(), - layout.BentoRightBottomPane: logs.NewLogsDetails(), - }, - layout.WithBentoLayoutCurrentPane(layout.BentoRightTopPane), - layout.WithBentoLayoutRightTopHeightRatio(0.5), +type LogPage interface { + tea.Model + layout.Sizeable + layout.Bindings +} +type logsPage struct { + width, height int + table layout.Container + details layout.Container +} + +func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + p.width = msg.Width + p.height = msg.Height + return p, p.SetSize(msg.Width, msg.Height) + } + + table, cmd := p.table.Update(msg) + cmds = append(cmds, cmd) + p.table = table.(layout.Container) + details, cmd := p.details.Update(msg) + cmds = append(cmds, cmd) + p.details = details.(layout.Container) + + return p, tea.Batch(cmds...) +} + +func (p *logsPage) View() string { + style := styles.BaseStyle.Width(p.width).Height(p.height) + return style.Render(lipgloss.JoinVertical(lipgloss.Top, + p.table.View(), + p.details.View(), + )) +} + +func (p *logsPage) BindingKeys() []key.Binding { + return p.table.BindingKeys() +} + +// GetSize implements LogPage. +func (p *logsPage) GetSize() (int, int) { + return p.width, p.height +} + +// SetSize implements LogPage. +func (p *logsPage) SetSize(width int, height int) tea.Cmd { + p.width = width + p.height = height + return tea.Batch( + p.table.SetSize(width, height/2), + p.details.SetSize(width, height/2), ) } + +func (p *logsPage) Init() tea.Cmd { + return tea.Batch( + p.table.Init(), + p.details.Init(), + ) +} + +func NewLogsPage() LogPage { + return &logsPage{ + table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)), + details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)), + } +} diff --git a/internal/tui/page/repl.go b/internal/tui/page/repl.go deleted file mode 100644 index 47a924b7b..000000000 --- a/internal/tui/page/repl.go +++ /dev/null @@ -1,21 +0,0 @@ -package page - -import ( - tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/tui/components/repl" - "github.com/kujtimiihoxha/termai/internal/tui/layout" -) - -var ReplPage PageID = "repl" - -func NewReplPage(app *app.App) tea.Model { - return layout.NewBentoLayout( - layout.BentoPanes{ - layout.BentoLeftPane: repl.NewSessionsCmp(app), - layout.BentoRightTopPane: repl.NewMessagesCmp(app), - layout.BentoRightBottomPane: repl.NewEditorCmp(app), - }, - layout.WithBentoLayoutCurrentPane(layout.BentoRightBottomPane), - ) -} |
