diff options
| author | adamdotdevin <[email protected]> | 2025-08-13 13:28:22 -0500 |
|---|---|---|
| committer | adamdotdevin <[email protected]> | 2025-08-13 13:28:22 -0500 |
| commit | 1357319f6f01532a3544a23d9282f9583a422608 (patch) | |
| tree | 1336779e02fea755bc45c8fc6d8f6db6ccbb9ccc /packages/tui | |
| parent | e729eed34d54758d4630012bf2396c064a71522e (diff) | |
| download | opencode-1357319f6f01532a3544a23d9282f9583a422608.tar.gz opencode-1357319f6f01532a3544a23d9282f9583a422608.zip | |
feat: bash commands
Diffstat (limited to 'packages/tui')
| -rw-r--r-- | packages/tui/internal/app/app.go | 79 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/editor.go | 23 | ||||
| -rw-r--r-- | packages/tui/internal/tui/tui.go | 31 |
3 files changed, 119 insertions, 14 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 48b7d67f9..08a79e310 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -49,6 +49,7 @@ type App struct { InitialSession *string compactCancel context.CancelFunc IsLeaderSequence bool + IsBashMode bool } func (a *App) Agent() *opencode.Agent { @@ -79,6 +80,9 @@ type AgentSelectedMsg struct { type SessionClearedMsg struct{} type CompactSessionMsg struct{} type SendPrompt = Prompt +type SendBash = struct { + Command string +} type SetEditorContentMsg struct { Text string } @@ -296,23 +300,41 @@ func (a *App) CycleRecentModel() (*App, tea.Cmd) { } nextIndex := 0 for i, recentModel := range recentModels { - if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID && recentModel.ModelID == a.Model.ID { + if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID && + recentModel.ModelID == a.Model.ID { nextIndex = (i + 1) % len(recentModels) break } } for range recentModels { currentRecentModel := recentModels[nextIndex%len(recentModels)] - provider, model := findModelByProviderAndModelID(a.Providers, currentRecentModel.ProviderID, currentRecentModel.ModelID) + provider, model := findModelByProviderAndModelID( + a.Providers, + currentRecentModel.ProviderID, + currentRecentModel.ModelID, + ) if provider != nil && model != nil { a.Provider, a.Model = provider, model - a.State.AgentModel[a.Agent().Name] = AgentModel{ProviderID: provider.ID, ModelID: model.ID} - return a, tea.Sequence(a.SaveState(), toast.NewSuccessToast(fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name))) + a.State.AgentModel[a.Agent().Name] = AgentModel{ + ProviderID: provider.ID, + ModelID: model.ID, + } + return a, tea.Sequence( + a.SaveState(), + toast.NewSuccessToast( + fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name), + ), + ) } - recentModels = append(recentModels[:nextIndex%len(recentModels)], recentModels[nextIndex%len(recentModels)+1:]...) + recentModels = append( + recentModels[:nextIndex%len(recentModels)], + recentModels[nextIndex%len(recentModels)+1:]...) if len(recentModels) < 2 { a.State.RecentlyUsedModels = recentModels - return a, tea.Sequence(a.SaveState(), toast.NewInfoToast("Not enough valid recent models to cycle")) + return a, tea.Sequence( + a.SaveState(), + toast.NewInfoToast("Not enough valid recent models to cycle"), + ) } } a.State.RecentlyUsedModels = recentModels @@ -464,10 +486,19 @@ func (a *App) InitializeProvider() tea.Cmd { // Priority 3: Current agent's preferred model if selectedProvider == nil && a.Agent().Model.ModelID != "" { - if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil && model != nil { + if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil && + model != nil { selectedProvider = provider selectedModel = model - slog.Debug("Selected model from current agent", "provider", provider.ID, "model", model.ID, "agent", a.Agent().Name) + slog.Debug( + "Selected model from current agent", + "provider", + provider.ID, + "model", + model.ID, + "agent", + a.Agent().Name, + ) } else { slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name) } @@ -724,6 +755,38 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) { return a, tea.Batch(cmds...) } +func (a *App) SendBash(ctx context.Context, command string) (*App, tea.Cmd) { + var cmds []tea.Cmd + if a.Session.ID == "" { + session, err := a.CreateSession(ctx) + if err != nil { + return a, toast.NewErrorToast(err.Error()) + } + a.Session = session + cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session})) + } + + cmds = append(cmds, func() tea.Msg { + _, err := a.Client.Session.Bash( + context.Background(), + a.Session.ID, + opencode.SessionBashParams{ + Agent: opencode.F(a.Agent().Name), + Command: opencode.F(command), + }, + ) + if err != nil { + slog.Error("Failed to submit bash command", "error", err) + return toast.NewErrorToast("Failed to submit bash command")() + } + return nil + }) + + // The actual response will come through SSE + // For now, just return success + return a, tea.Batch(cmds...) +} + func (a *App) Cancel(ctx context.Context, sessionID string) error { // Cancel any running compact operation if a.compactCancel != nil { diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 980d5262c..a51f158ce 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -39,6 +39,7 @@ type EditorComponent interface { Focus() (tea.Model, tea.Cmd) Blur() Submit() (tea.Model, tea.Cmd) + SubmitBash() (tea.Model, tea.Cmd) Clear() (tea.Model, tea.Cmd) Paste() (tea.Model, tea.Cmd) Newline() (tea.Model, tea.Cmd) @@ -342,6 +343,14 @@ func (m *editorComponent) Content() string { Padding(0, 0, 0, 1). Bold(true) prompt := promptStyle.Render(">") + borderForeground := t.Border() + if m.app.IsLeaderSequence { + borderForeground = t.Accent() + } + if m.app.IsBashMode { + borderForeground = t.Secondary() + prompt = promptStyle.Render("!") + } m.textarea.SetWidth(width - 6) textarea := lipgloss.JoinHorizontal( @@ -349,10 +358,6 @@ func (m *editorComponent) Content() string { prompt, m.textarea.View(), ) - borderForeground := t.Border() - if m.app.IsLeaderSequence { - borderForeground = t.Accent() - } textarea = styles.NewStyle(). Background(t.BackgroundElement()). Width(width). @@ -489,6 +494,16 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } +func (m *editorComponent) SubmitBash() (tea.Model, tea.Cmd) { + command := m.textarea.Value() + var cmds []tea.Cmd + updated, cmd := m.Clear() + m = updated.(*editorComponent) + cmds = append(cmds, cmd) + cmds = append(cmds, util.CmdHandler(app.SendBash{Command: command})) + return m, tea.Batch(cmds...) +} + func (m *editorComponent) Clear() (tea.Model, tea.Cmd) { m.textarea.Reset() m.historyIndex = -1 diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 0262f5077..f57470840 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -151,6 +151,23 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + if a.app.IsBashMode { + if keyString == "backspace" && a.editor.Length() == 0 { + a.app.IsBashMode = false + return a, nil + } + + if keyString == "enter" || keyString == "esc" || keyString == "ctrl+c" { + a.app.IsBashMode = false + if keyString == "enter" { + updated, cmd := a.editor.SubmitBash() + a.editor = updated.(chat.EditorComponent) + cmds = append(cmds, cmd) + } + return a, tea.Batch(cmds...) + } + } + // 1. Handle active modal if a.modal != nil { switch keyString { @@ -189,7 +206,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // 3. Handle completions trigger if keyString == "/" && !a.showCompletionDialog && - a.editor.Value() == "" { + a.editor.Value() == "" && + !a.app.IsBashMode { a.showCompletionDialog = true updated, cmd := a.editor.Update(msg) @@ -207,7 +225,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Handle file completions trigger if keyString == "@" && - !a.showCompletionDialog { + !a.showCompletionDialog && + !a.app.IsBashMode { a.showCompletionDialog = true updated, cmd := a.editor.Update(msg) @@ -223,6 +242,11 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, tea.Sequence(cmds...) } + if keyString == "!" && a.editor.Value() == "" { + a.app.IsBashMode = true + return a, nil + } + if a.showCompletionDialog { switch keyString { case "tab", "enter", "esc", "ctrl+c", "up", "down", "ctrl+p", "ctrl+n": @@ -378,6 +402,9 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showCompletionDialog = false a.app, cmd = a.app.SendPrompt(context.Background(), msg) cmds = append(cmds, cmd) + case app.SendBash: + a.app, cmd = a.app.SendBash(context.Background(), msg.Command) + cmds = append(cmds, cmd) case app.SetEditorContentMsg: // Set the editor content without sending a.editor.SetValueWithAttachments(msg.Text) |
