summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/page
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-04-21 19:59:35 +0200
committerGitHub <[email protected]>2025-04-21 19:59:35 +0200
commitf33dff87725764af0b675b5e5b2e011b21c14c90 (patch)
tree4fe2c022305f13775f2cab3cdd80cd808259765b /internal/tui/page
parent6b1c64bcc75b89c530294b6a2d4404682b435d56 (diff)
parent3a6a26981a8074b6ab0eaadb520db986e04799ff (diff)
downloadopencode-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.go167
-rw-r--r--internal/tui/page/init.go308
-rw-r--r--internal/tui/page/logs.go83
-rw-r--r--internal/tui/page/repl.go21
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),
- )
-}